Plugin Directory

Changeset 3765552


Ignore:
Timestamp:
10/26/2026 04:50:29 AM (63 minutes ago)
Author:
ninhle
Message:

Release 2.6.0

  • Add to cart in the chat: product cards with photo, price and stock; WooCommerce updates the cart in the shopper's browser
  • Order tracking cards with tracking numbers; "my orders" for logged-in customers
  • Replies in the shopper's language
  • Sales Insights: unanswered questions, searches with no result, out-of-stock demand
  • Ready for AI agents (Abilities API, WordPress 6.9+); WordPress AI connection (WordPress 7.0+)
  • AI replies run in the background; new-replies badge on the chat button
  • Live Preview blueprint (assets/blueprints/blueprint.json)
Location:
ninhle-ai-sales-assistant
Files:
96 added
6 deleted
112 edited
1 copied

Legend:

Unmodified
Added
Removed
  • ninhle-ai-sales-assistant/tags/2.6.0/assets/dist/admin.bundle.js

    r3761175 r3765552  
    1 import{C as Q,l as P,k as be,S as z,F as ne,R as Ie,d as p,A as Y,q as re,u as e,h as j}from"./assets/hooks.module-C33f2O-M.js";function Ge(r,t){for(var s in t)r[s]=t[s];return r}function ge(r,t){for(var s in r)if(s!=="__source"&&!(s in t))return!0;for(var a in t)if(a!=="__source"&&r[a]!==t[a])return!0;return!1}function fe(r,t){this.props=r,this.context=t}(fe.prototype=new Q).isPureReactComponent=!0,fe.prototype.shouldComponentUpdate=function(r,t){return ge(this.props,r)||ge(this.state,t)};var ye=P.__b;P.__b=function(r){r.type&&r.type.__f&&r.ref&&(r.props.ref=r.ref,r.ref=null),ye&&ye(r)};var Je=P.__e;P.__e=function(r,t,s,a){if(r.then){for(var o,d=t;d=d.__;)if((o=d.__c)&&o.__c)return t.__e==null&&(t.__e=s.__e,t.__k=s.__k||[]),o.__c(r,t)}Je(r,t,s,a)};var ve=P.unmount;function Pe(r,t,s){return r&&(r.__c&&r.__c.__H&&(r.__c.__H.__.forEach(function(a){typeof a.__c=="function"&&a.__c()}),r.__c.__H=null),(r=Ge({},r)).__c!=null&&(r.__c.__P===s&&(r.__c.__P=t),r.__c.__e=!0,r.__c=null),r.__k=r.__k&&r.__k.map(function(a){return Pe(a,t,s)})),r}function Oe(r,t,s){return r&&s&&(r.__v=null,r.__k=r.__k&&r.__k.map(function(a){return Oe(a,t,s)}),r.__c&&r.__c.__P===t&&(r.__e&&s.appendChild(r.__e),r.__c.__e=!0,r.__c.__P=s)),r}function se(){this.__u=0,this.o=null,this.__b=null}function Re(r){var t=r.__&&r.__.__c;return t&&t.__a&&t.__a(r)}function te(){this.i=null,this.l=null}P.unmount=function(r){var t=r.__c;t&&(t.__z=!0),t&&t.__R&&t.__R(),t&&32&r.__u&&(r.type=null),ve&&ve(r)},(se.prototype=new Q).__c=function(r,t){var s=t.__c,a=this;a.o==null&&(a.o=[]),a.o.push(s);var o=Re(a.__v),d=!1,i=function(){d||a.__z||(d=!0,s.__R=null,o?o(b):b())};s.__R=i;var f=s.__P;s.__P=null;var b=function(){if(!--a.__u){if(a.state.__a){var _=a.state.__a;a.__v.__k[0]=Oe(_,_.__c.__P,_.__c.__O)}var E;for(a.setState({__a:a.__b=null});E=a.o.pop();)E.__P=f,E.forceUpdate()}};a.__u++||32&t.__u||a.setState({__a:a.__b=a.__v.__k[0]}),r.then(i,i)},se.prototype.componentWillUnmount=function(){this.o=[]},se.prototype.render=function(r,t){if(this.__b){if(this.__v.__k){var s=document.createElement("div"),a=this.__v.__k[0].__c;this.__v.__k[0]=Pe(this.__b,s,a.__O=a.__P)}this.__b=null}var o=t.__a&&be(z,null,r.fallback);return o&&(o.__u&=-33),[be(z,null,t.__a?null:r.children),o]};var we=function(r,t,s){if(++s[1]===s[0]&&r.l.delete(t),r.props.revealOrder&&(r.props.revealOrder[0]!=="t"||!r.l.size))for(s=r.i;s;){for(;s.length>3;)s.pop()();if(s[1]<s[0])break;r.i=s=s[2]}};(te.prototype=new Q).__a=function(r){var t=this,s=Re(t.__v),a=t.l.get(r);return a[0]++,function(o){var d=function(){t.props.revealOrder?(a.push(o),we(t,r,a)):o()};s?s(d):d()}},te.prototype.render=function(r){this.i=null,this.l=new Map;var t=ne(r.children);r.revealOrder&&r.revealOrder[0]==="b"&&t.reverse();for(var s=t.length;s--;)this.l.set(t[s],this.i=[1,0,this.i]);return r.children},te.prototype.componentDidUpdate=te.prototype.componentDidMount=function(){var r=this;this.l.forEach(function(t,s){we(r,s,t)})};var Ke=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.element")||60103,Ye=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,Ze=/^on(Ani|Tra|Tou|BeforeInp|Compo)/,Xe=/[A-Z0-9]/g,Qe=typeof document<"u",et=function(r){return(typeof Symbol<"u"&&typeof Symbol()=="symbol"?/fil|che|rad/:/fil|che|ra/).test(r)};function tt(r,t,s){return t.__k==null&&(t.textContent=""),Ie(r,t),typeof s=="function"&&s(),r?r.__c:null}Q.prototype.isReactComponent=!0,["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(r){Object.defineProperty(Q.prototype,r,{configurable:!0,get:function(){return this["UNSAFE_"+r]},set:function(t){Object.defineProperty(this,r,{configurable:!0,writable:!0,value:t})}})});var ke=P.event;P.event=function(r){return ke&&(r=ke(r)),r.persist=function(){},r.isPropagationStopped=function(){return this.cancelBubble},r.isDefaultPrevented=function(){return this.defaultPrevented},r.nativeEvent=r};var rt={configurable:!0,get:function(){return this.class}},Ne=P.vnode;P.vnode=function(r){typeof r.type=="string"&&function(t){var s=t.props,a=t.type,o={},d=a.indexOf("-")==-1;for(var i in s){var f=s[i];if(!(i==="value"&&"defaultValue"in s&&f==null||Qe&&i==="children"&&a==="noscript"||i==="class"||i==="className")){var b=i.toLowerCase();i==="defaultValue"&&"value"in s&&s.value==null?i="value":i==="download"&&f===!0?f="":b==="translate"&&f==="no"?f=!1:b[0]==="o"&&b[1]==="n"?b==="ondoubleclick"?i="ondblclick":b!=="onchange"||a!=="input"&&a!=="textarea"||et(s.type)?b==="onfocus"?i="onfocusin":b==="onblur"?i="onfocusout":Ze.test(i)&&(i=b):b=i="oninput":d&&Ye.test(i)?i=i.replace(Xe,"-$&").toLowerCase():f===null&&(f=void 0),b==="oninput"&&o[i=b]&&(i="oninputCapture"),o[i]=f}}a=="select"&&(o.multiple&&Array.isArray(o.value)&&(o.value=ne(s.children).forEach(function(_){_.props.selected=o.value.indexOf(_.props.value)!=-1})),o.defaultValue!=null&&(o.value=ne(s.children).forEach(function(_){_.props.selected=o.multiple?o.defaultValue.indexOf(_.props.value)!=-1:o.defaultValue==_.props.value}))),s.class&&!s.className?(o.class=s.class,Object.defineProperty(o,"className",rt)):s.className&&(o.class=o.className=s.className),t.props=o}(r),r.$$typeof=Ke,Ne&&Ne(r)};var xe=P.__r;P.__r=function(r){xe&&xe(r),r.__c};var Ce=P.diffed;P.diffed=function(r){Ce&&Ce(r);var t=r.props,s=r.__e;s!=null&&r.type==="textarea"&&"value"in t&&t.value!==s.value&&(s.value=t.value==null?"":t.value)};function st(r){return!!r.__k&&(Ie(null,r),!0)}function Be(r){return{render:function(t){tt(t,r)},unmount:function(){st(r)}}}var Ee;const x=((Ee=window.SMBCommerceAdmin)==null?void 0:Ee.restUrl)||"/wp-json/smb-commerce/v1";async function R(r){const t=await r.text();return t?JSON.parse(t):null}const N={getHeaders(){var t;return{"Content-Type":"application/json",Accept:"application/json","X-WP-Nonce":((t=window.SMBCommerceAdmin)==null?void 0:t.nonce)||""}},async get(r){const t=await fetch(`${x}${r}`,{method:"GET",headers:this.getHeaders()});if(!t.ok)throw new Error(`GET ${r} failed`);return await R(t)},async post(r,t={}){const s=await fetch(`${x}${r}`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify(t)});if(!s.ok)throw new Error(`POST ${r} failed`);return await R(s)},async delete(r){const t=await fetch(`${x}${r}`,{method:"DELETE",headers:this.getHeaders()});if(!t.ok)throw new Error(`DELETE ${r} failed`);return await R(t)},async getConversations(r,t,s=1){const a=new URL(`${x}/admin/conversations`,window.location.origin);r&&a.searchParams.append("status",r),t!==void 0&&a.searchParams.append("assigned_agent_id",t.toString()),a.searchParams.append("page",s.toString());const o=await fetch(a.toString(),{method:"GET",headers:this.getHeaders()});if(!o.ok)throw new Error("Failed to fetch conversations");return await R(o)},async getMessages(r,t=null){const s=new URL(`${x}/admin/conversations/${r}/messages`,window.location.origin);t&&s.searchParams.append("last_message_id",t.toString());const a=await fetch(s.toString(),{method:"GET",headers:this.getHeaders()});if(!a.ok)throw new Error("Failed to fetch messages");return await R(a)},async sendMessage(r,t,s){const a=await fetch(`${x}/admin/conversations/${r}/message`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify({content:t,client_message_id:s})});if(!a.ok)throw new Error("Failed to send message");return await R(a)},async takeover(r){const t=await fetch(`${x}/admin/conversations/${r}/takeover`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to takeover conversation");return await R(t)},async release(r){const t=await fetch(`${x}/admin/conversations/${r}/release`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to release conversation");return await R(t)},async close(r){const t=await fetch(`${x}/admin/conversations/${r}/close`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to close conversation");return await R(t)},async deleteConversation(r){const t=await fetch(`${x}/admin/conversations/${r}`,{method:"DELETE",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to delete conversation");return await R(t)},async getLeads(r=1,t=20){const s=new URL(`${x}/admin/leads`,window.location.origin);s.searchParams.append("page",r.toString()),s.searchParams.append("per_page",t.toString());const a=await fetch(s.toString(),{method:"GET",headers:this.getHeaders()});if(!a.ok)throw new Error("Failed to fetch leads");return await R(a)},async bulkDeleteConversations(r){const t=await fetch(`${x}/admin/conversations/bulk-delete`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify({ids:r})});if(!t.ok)throw new Error("Bulk delete failed");return await R(t)}};function ce(){const[r,t]=p([]),s=Y(1),a=re(d=>t(i=>i.filter(f=>f.id!==d)),[]),o=re((d,i)=>{const f=s.current++;t(b=>[...b.filter(_=>_.text!==i),{id:f,type:d,text:i}]),(d==="success"||d==="info")&&window.setTimeout(()=>a(f),6e3)},[a]);return{notices:r,push:o,dismiss:a}}function ie({notices:r,onDismiss:t}){return r.length===0?null:e("div",{className:"smb-notices",children:r.map(s=>e("div",{className:`notice notice-${s.type} is-dismissible smb-inline-notice`,role:s.type==="error"?"alert":"status",children:[e("p",{children:s.text}),e("button",{type:"button",className:"notice-dismiss",onClick:()=>t(s.id),children:e("span",{className:"screen-reader-text",children:"Dismiss this notice."})})]},s.id))})}function le(){const[r,t]=p(null),s=re((o,d,i)=>{t({message:o,confirmLabel:d,onConfirm:i})},[]),a=re(()=>t(null),[]);return{request:r,ask:s,cancel:a}}function me({request:r,onCancel:t}){const s=Y(null),[a,o]=p(!1);if(j(()=>{var i;r&&((i=s.current)==null||i.focus())},[r]),!r)return null;const d=async()=>{o(!0);try{await r.onConfirm()}finally{o(!1),t()}};return e("div",{className:"smb-confirm",role:"alertdialog","aria-modal":"false","aria-labelledby":"smb-confirm-text",onKeyDown:i=>{i.key==="Escape"&&t()},children:[e("p",{id:"smb-confirm-text",children:r.message}),e("div",{className:"smb-confirm-actions",children:[e("button",{type:"button",className:"button button-primary smb-button-danger",onClick:d,disabled:a,children:r.confirmLabel}),e("button",{type:"button",className:"button",ref:s,onClick:t,disabled:a,children:"Cancel"})]})]})}const nt={bot_active:"AI assistant",human_active:"Waiting for your team",awaiting_agent:"Waiting for your team",agent_active:"Team member replying",closed:"Closed"},at={webchat:"Website chat"},Se=r=>nt[r]||r,ot=r=>r.sender_type==="user"?"Customer":r.sender_type==="agent"?r.first_name?`${r.first_name} (team)`:"Team member":r.sender_type==="system"?"Assistant":r.sender_type;function ct(){const{notices:r,push:t,dismiss:s}=ce(),a=le(),[o,d]=p([]),[i,f]=p(null),[b,_]=p([]),[E,y]=p(""),[w,$]=p(new Set),[B,I]=p(!1),D=Y(null),C=Y(5e3),u=Y(null),h=Y(null);j(()=>{S()},[]);const S=async()=>{try{const m=await N.getConversations();d(m.items||[])}catch(m){console.error(m)}},H=async m=>{var k;f(m);try{const T=await N.getMessages(m.id);_(T.messages||[]),h.current=((k=T.messages)==null?void 0:k.length)>0?T.messages[T.messages.length-1].id:null,U()}catch(T){console.error(T)}},U=()=>setTimeout(()=>{var m;return(m=D.current)==null?void 0:m.scrollIntoView({behavior:"smooth"})},100),O=async()=>{var m;if(!i||document.visibilityState!=="visible"){u.current=window.setTimeout(O,C.current);return}try{const k=await N.getMessages(i.id,h.current);((m=k.messages)==null?void 0:m.length)>0&&(_(T=>{const v=new Set(k.messages.map(M=>M.id));return[...T.filter(M=>!v.has(M.id)),...k.messages]}),h.current=k.messages[k.messages.length-1].id,U(),C.current=5e3)}catch{C.current=Math.min(C.current*2,6e4)}u.current=window.setTimeout(O,C.current)};j(()=>(i&&(u.current=window.setTimeout(O,C.current)),()=>{u.current&&clearTimeout(u.current)}),[i]);const V=async()=>{if(i)try{await N.takeover(i.id),f({...i,status:"agent_active"}),S()}catch{t("error","Takeover failed. Another agent may already have this conversation.")}},A=async()=>{if(i)try{await N.release(i.id),f({...i,status:"bot_active"}),S()}catch{t("error","Could not hand the conversation back to the bot.")}},q=(m,k)=>{k.stopPropagation(),a.ask(`Permanently delete conversation #${m}? This cannot be undone.`,"Delete",async()=>{try{await N.deleteConversation(m),(i==null?void 0:i.id)===m&&(f(null),_([])),$(T=>{const v=new Set(T);return v.delete(m),v}),S(),t("success",`Conversation #${m} deleted.`)}catch{t("error","Could not delete the conversation.")}})},K=(m,k)=>{k.stopPropagation(),$(T=>{const v=new Set(T);return v.has(m)?v.delete(m):v.add(m),v})},Z=()=>{w.size===o.length&&o.length>0?$(new Set):$(new Set(o.map(m=>m.id)))},g=()=>{if(w.size===0)return;const m=w.size;a.ask(`Permanently delete ${m} selected conversation(s)? This cannot be undone.`,`Delete ${m}`,async()=>{I(!0);try{const k=await N.bulkDeleteConversations(Array.from(w));i&&w.has(i.id)&&(f(null),_([])),$(new Set),S(),k.failed>0?t("warning",`${k.failed} conversation(s) could not be deleted.`):t("success",`${m} conversation(s) deleted.`)}catch{t("error","Bulk delete failed.")}finally{I(!1)}})},G=async()=>{if(!E.trim()||!i)return;const m=E.trim();y("");const k=crypto.randomUUID(),T={id:-Date.now(),sender_type:"agent",content:m,created_at:new Date().toISOString()};_(v=>[...v,T]),U();try{const v=await N.sendMessage(i.id,m,k);_(M=>M.map(ee=>ee.id===T.id?v:ee)),h.current=v.id}catch{t("error","Could not send the message. Your text is back in the reply box."),_(v=>v.filter(M=>M.id!==T.id)),y(m)}},W={bot_active:"#10b981",human_active:"#f59e0b",agent_active:"#3b82f6",closed:"#9ca3af"};return e(z,{children:[e(ie,{notices:r,onDismiss:s}),e(me,{request:a.request,onCancel:a.cancel}),e("div",{className:"smb-commerce-inbox-container",children:[e("div",{className:"smb-commerce-sidebar",children:[e("div",{style:{padding:"12px 15px",borderBottom:"1px solid #ccd0d4",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("h2",{style:{margin:0,fontSize:"15px",fontWeight:700},children:["Conversations (",o.length,")"]}),e("button",{className:"button",style:{fontSize:"12px",padding:"3px 8px"},onClick:Z,children:w.size===o.length&&o.length>0?"Deselect all":"Select all"})]}),w.size>0&&e("div",{style:{padding:"8px 12px",background:"#fff3cd",borderBottom:"1px solid #f0c36d",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("span",{style:{fontSize:"13px",fontWeight:600,color:"#856404"},children:["Selected: ",w.size]}),e("button",{style:{background:"#d63638",color:"white",border:"none",fontSize:"12px",padding:"4px 12px",borderRadius:"4px",cursor:"pointer",fontWeight:600},onClick:g,disabled:B,children:B?"Deleting…":"Delete "+w.size})]}),e("ul",{className:"smb-commerce-conversation-list",style:{listStyle:"none",margin:0,padding:0},children:o.map(m=>e("li",{className:"smb-commerce-conversation-item "+((i==null?void 0:i.id)===m.id?"active":""),onClick:()=>H(m),style:{display:"flex",alignItems:"center",gap:"8px",padding:"10px 12px",background:w.has(m.id)?"#eff6ff":void 0},children:[e("input",{type:"checkbox",checked:w.has(m.id),onClick:k=>K(m.id,k),onChange:()=>{},style:{width:"15px",height:"15px",flexShrink:0,cursor:"pointer"}}),e("div",{style:{flex:1,minWidth:0},children:[e("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"4px"},children:[e("span",{style:{fontWeight:600,fontSize:"13px"},children:["Conversation #",m.id]}),e("span",{style:{fontSize:"10px",fontWeight:600,color:"white",background:W[m.status]||"#9ca3af",padding:"1px 6px",borderRadius:"10px",whiteSpace:"nowrap"},children:Se(m.status)})]}),e("div",{style:{fontSize:"11px",color:"#666",marginTop:"2px"},children:at[m.channel]||m.channel})]}),e("button",{style:{color:"#d63638",padding:"2px",background:"none",border:"none",cursor:"pointer",flexShrink:0},onClick:k=>q(m.id,k),title:"Delete conversation","aria-label":`Delete conversation #${m.id}`,children:e("span",{className:"dashicons dashicons-trash",style:{fontSize:"16px"}})})]},m.id))})]}),e("div",{className:"smb-commerce-main",children:i?e(z,{children:[e("div",{className:"smb-commerce-main-header",children:[e("h3",{children:["Conversation #",i.id," — ",e("span",{style:{color:W[i.status]},children:Se(i.status)})]}),e("div",{className:"actions",children:[i.status!=="agent_active"&&e("button",{onClick:V,className:"button button-primary",children:"Take over"}),i.status==="agent_active"&&e("button",{onClick:A,className:"button button-secondary",children:"Hand back to bot"})]})]}),e("div",{className:"smb-commerce-chat-history",children:[b.map(m=>e("div",{className:"smb-commerce-admin-msg "+m.sender_type,children:[e("div",{className:"msg-sender",children:ot(m)}),e("div",{className:"msg-content",children:m.content})]},m.id)),e("div",{ref:D})]}),e("div",{className:"smb-commerce-main-input",children:[e("input",{type:"text",value:E,onInput:m=>y(m.target.value),onKeyPress:m=>m.key==="Enter"&&G(),placeholder:"Type your reply…",disabled:i.status!=="agent_active"&&i.status!=="awaiting_agent"}),e("button",{className:"button button-primary",onClick:G,disabled:i.status!=="agent_active"&&i.status!=="awaiting_agent",children:"Send"})]})]}):e("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",color:"#666"},children:"Select a conversation to get started"})})]})]})}const it={"message.created":"Visitor sends a message","conversation.created":"Visitor opens the chat"},lt={content:"Message text",time:"Site hour"},Te={soft:"normal priority",hard:"high priority",emergency:"urgent"},De=r=>{if(typeof r!="string")return r||{};try{return JSON.parse(r)||{}}catch{return{}}},mt=r=>{const t=De(r),s=t.conditions;if(!Array.isArray(s)||s.length===0)return"Always";const a=o=>String(o??"").split(",").map(d=>d.trim()).filter(Boolean).map(d=>`"${d}"`).join(" or ");return s.map(o=>{if(o.type==="message_contains")return`Message text contains "${o.value}"`;if(o.type==="conversation.has_tag")return`Conversation has the tag "${o.value}"`;const d=lt[o.field]||o.field;return o.operator==="contains"?`${d} contains ${a(o.value)}`:o.operator==="equals"?`${d} is exactly "${o.value}"`:o.operator==="after"?`${d} is ${o.value}:00 or later`:JSON.stringify(o)}).join(t.type==="or"?" or ":" and ")},dt=r=>{const t=De(r).actions;return!Array.isArray(t)||t.length===0?JSON.stringify(r):t.map(s=>s.type==="handoff"?`Hand off to a person (${Te[String(s.value||"").toLowerCase()]||Te.soft})`:s.type==="send_message"?`Reply: "${s.value}"`:s.type==="tag_conversation"?`Add the tag "${s.value}"`:JSON.stringify(s)).join(", then ")},ht=()=>{const{notices:r,push:t,dismiss:s}=ce(),a=le(),[o,d]=p(""),[i,f]=p([]),[b,_]=p(!0),[E,y]=p(!1),[w,$]=p(""),[B,I]=p("message.created"),[D,C]=p("content"),[u,h]=p("contains"),[S,H]=p(""),[U,O]=p("handoff"),[V,A]=p("Soft"),q=async()=>{_(!0);try{const g=await N.get("/admin/rules");g.items&&f(g.items)}catch(g){console.error("Failed to load rules",g)}_(!1)};j(()=>{q()},[]);const K=(g,G)=>{a.ask(`Delete the rule "${G}"?`,"Delete rule",async()=>{try{await N.delete(`/admin/rules/${g}`),q(),t("success","Rule deleted.")}catch{t("error","Could not delete the rule.")}})},Z=async()=>{if(!w.trim()){d("Please enter a rule name.");return}d("");const g={name:w,trigger_event:B,conditions:{type:"and",conditions:[{type:"simple",field:D,operator:u,value:S}]},actions:{actions:[{type:U,value:V}]},priority:10,is_active:1};try{await N.post("/admin/rules",g),y(!1),$(""),H(""),q(),t("success","Rule saved and enabled.")}catch{t("error","Could not save the rule.")}};return b?e("div",{role:"status",children:"Loading…"}):e("div",{className:"smb-commerce-rules",children:[e(ie,{notices:r,onDismiss:s}),e(me,{request:a.request,onCancel:a.cancel}),e("h2",{children:"Automation rules"}),e("p",{children:"Automatically run an action when a visitor sends a message or opens the chat, based on conditions you define."}),E?e("div",{className:"rule-form",style:{background:"#fff",padding:"20px",border:"1px solid #ccd0d4",borderRadius:"4px",maxWidth:"800px"},children:[e("h3",{style:{marginTop:0},children:"Create an automation rule"}),e("div",{style:{marginBottom:15},children:[e("label",{htmlFor:"smb-rule-name",style:{fontWeight:600},children:"Rule name:"}),e("br",{}),e("input",{id:"smb-rule-name",type:"text",className:"regular-text",style:{width:"100%",maxWidth:"400px"},value:w,onChange:g=>{$(g.target.value),o&&d("")},"aria-invalid":o?!0:void 0,"aria-describedby":o?"smb-rule-name-error":void 0,placeholder:"e.g. Hand off when a visitor complains"}),o&&e("p",{id:"smb-rule-name-error",className:"smb-field-error",role:"alert",children:o})]}),e("div",{style:{marginBottom:15},children:[e("label",{style:{fontWeight:600},children:"1. When this happens (trigger):"}),e("br",{}),e("select",{value:B,onChange:g=>I(g.target.value),style:{width:"100%",maxWidth:"400px"},children:[e("option",{value:"message.created",children:"Visitor sends a new message"}),e("option",{value:"conversation.created",children:"Visitor opens the chat for the first time"})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #2271b1"},children:[e("label",{style:{fontWeight:600},children:"2. AND these conditions match (if):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:D,onChange:g=>C(g.target.value),children:[e("option",{value:"content",children:"Message text"}),e("option",{value:"time",children:"Current time (site hour, 0–23)"})]}),e("select",{value:u,onChange:g=>h(g.target.value),children:[e("option",{value:"contains",children:"Contains keyword"}),e("option",{value:"equals",children:"Exactly matches"}),e("option",{value:"after",children:"After (hour)"})]}),e("input",{type:"text",value:S,onChange:g=>H(g.target.value),placeholder:"e.g. refund, quote..."})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #00a32a"},children:[e("label",{style:{fontWeight:600},children:"3. THEN do this (then):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:U,onChange:g=>O(g.target.value),children:[e("option",{value:"handoff",children:"Hand off to a human agent"}),e("option",{value:"send_message",children:"Send an automatic message"}),e("option",{value:"tag_conversation",children:"Add a tag"})]}),e("input",{type:"text",value:V,onChange:g=>A(g.target.value),placeholder:"e.g. Soft (or the message text)",style:{flex:1}})]})]}),e("div",{style:{marginTop:20,paddingTop:"15px",borderTop:"1px solid #ddd"},children:[e("button",{onClick:Z,className:"button button-primary",children:"Save and enable"}),e("button",{onClick:()=>y(!1),className:"button",style:{marginLeft:10},children:"Cancel"})]})]}):e(z,{children:[e("button",{onClick:()=>y(!0),className:"button button-primary",style:{marginBottom:15},children:"+ New rule"}),e("table",{className:"wp-list-table widefat fixed striped",children:[e("thead",{children:e("tr",{children:[e("th",{children:"Rule name"}),e("th",{children:"When"}),e("th",{children:"If"}),e("th",{children:"Then"}),e("th",{children:"Status"}),e("th",{children:"Actions"})]})}),e("tbody",{children:[i.map(g=>e("tr",{children:[e("td",{children:e("strong",{children:g.name})}),e("td",{children:it[g.trigger_event]||g.trigger_event}),e("td",{children:mt(g.conditions)}),e("td",{children:dt(g.actions)}),e("td",{children:g.is_active?e("span",{style:{color:"green"},children:"Active"}):e("span",{style:{color:"gray"},children:"Disabled"})}),e("td",{children:e("button",{type:"button",onClick:()=>K(g.id,g.name),className:"button-link button-link-delete",children:"Delete"})})]},g.id)),i.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No rules created yet."})})]})]})]})]})},ut=()=>{const[r,t]=p([]),[s,a]=p(!0),o=async()=>{a(!0);try{const i=await N.getLeads();i.items&&t(i.items)}catch(i){console.error("Failed to load leads",i)}a(!1)};return j(()=>{o()},[]),s?e("div",{children:"Loading leads..."}):e("div",{className:"smb-commerce-leads",children:[e("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e("h2",{children:"Leads"}),e("button",{className:"button button-primary",onClick:()=>{var f,b;const i=`${((f=window.SMBCommerceAdmin)==null?void 0:f.restUrl)||"/wp-json/smb-commerce/v1"}/admin/leads/export?_wpnonce=${((b=window.SMBCommerceAdmin)==null?void 0:b.nonce)||""}`;window.location.href=i},children:[e("span",{className:"dashicons dashicons-download",style:{marginTop:"4px"}})," Export CSV"]})]}),e("table",{className:"wp-list-table widefat fixed striped",style:{marginTop:"10px"},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Name"}),e("th",{children:"Email"}),e("th",{children:"Phone"}),e("th",{children:"Score"}),e("th",{children:"Status"}),e("th",{children:"Created At"})]})}),e("tbody",{children:[r.map(i=>e("tr",{children:[e("td",{children:i.name||"-"}),e("td",{children:i.email||"-"}),e("td",{children:i.phone||"-"}),e("td",{children:i.score}),e("td",{children:i.status}),e("td",{children:new Date(i.created_at).toLocaleString()})]},i.id)),r.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No leads found."})})]})]})]})};function pt(){var ue,pe,_e;const r=le(),[t,s]=p({smbcommerce_brand_name:"Shop Assistant",smbcommerce_primary_color:"#01696f",smbcommerce_logo_url:"",smbcommerce_font_family:"system",smbcommerce_runtime_mode:"smart_hybrid",smbcommerce_ai_provider:"openai",smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:"",smbcommerce_telegram_chat_id:"",smbcommerce_sales_playbook:"",smbcommerce_business_info:"",smbcommerce_enable_adaptive_playbook:"1",smbcommerce_meta_prompt_template:"",smbcommerce_enable_prompt_creator:"1",smbcommerce_prompt_creator_template:"",smbcommerce_max_upload_size:"5",smbcommerce_handoff_webhook:"",smbcommerce_system_guardrails_prompt:"",smbcommerce_media_retention_days:"30",smbcommerce_widget_slogan:"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:"",smbcommerce_hotline:"",smbcommerce_zalo_link:"",smbcommerce_messenger_link:"",smbcommerce_whatsapp:"",smbcommerce_telegram_link:"",smbcommerce_ai_notice_enabled:"1",smbcommerce_ai_notice_text:"",smbcommerce_hide_on_checkout:"0",smbcommerce_voice_input_enabled:"0",smbcommerce_gemini_model:"",smbcommerce_anthropic_model:"",smbcommerce_openai_model:"",smbcommerce_refund_policy:"",smbcommerce_warranty_policy:"",smbcommerce_delivery_policy:"",smbcommerce_privacy_policy:"",smbcommerce_enable_link_reading:"0",smbcommerce_budget_daily_usd:"0",smbcommerce_budget_monthly_usd:"0",smbcommerce_enable_provider_fallback:"1",smbcommerce_block_ungrounded_prices:"0",smbcommerce_enable_response_cache:"0",smbcommerce_cache_ttl_hours:"24",smbcommerce_webhook_url:"",smbcommerce_webhook_secret:""}),[a,o]=p({openai:!1,anthropic:!1,gemini:!1,telegram:!1}),[d,i]=p(!1),[f,b]=p(""),[_,E]=p(null),[y,w]=p(!1),[$,B]=p(!1),[I,D]=p(null),[C,u]=p(null),[h,S]=p(null),[H,U]=p(30),[O,V]=p(null),[A,q]=p(null),[K,Z]=p({}),[g,G]=p(!0),[W,m]=p(null),[k,T]=p(null),v=n=>!K[n]||K[n].allowed;j(()=>{de(),he()},[]),j(()=>{M(H)},[H]);const M=async n=>{try{const c=await fetch(`${x}/admin/analytics?days=${n}`,{headers:N.getHeaders()});S(await c.json())}catch(c){console.error("Analytics load failed",c)}},ee=async()=>{V({pending:!0});try{const n=await fetch(`${x}/admin/webhook/test`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})});V(await n.json())}catch(n){V({message:n.message})}},Me=async()=>{try{await fetch(`${x}/admin/cache/flush`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})}),M(H),b("Cache cleared."),setTimeout(()=>b(""),3e3)}catch{b("Could not clear the cache.")}},J=n=>"$"+(Number(n)||0).toFixed(n<1?4:2);j(()=>{const n=t.smbcommerce_font_family;if(!n||n==="system")return;const c=`smb-commerce-font-${n.replace(/\s+/g,"-")}`;if(!document.getElementById(c)){const L=document.createElement("link");L.id=c,L.rel="stylesheet",L.href=`https://fonts.googleapis.com/css2?family=${n.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(L)}},[t.smbcommerce_font_family]);const de=async()=>{try{const c=await(await fetch(`${x}/admin/settings`,{headers:N.getHeaders()})).json();c.settings&&(s(L=>({...L,smbcommerce_brand_name:c.settings.smbcommerce_brand_name||"",smbcommerce_primary_color:c.settings.smbcommerce_primary_color||"",smbcommerce_logo_url:c.settings.smbcommerce_logo_url||"",smbcommerce_font_family:c.settings.smbcommerce_font_family||"system",smbcommerce_runtime_mode:c.settings.smbcommerce_runtime_mode||"smart_hybrid",smbcommerce_ai_provider:c.settings.smbcommerce_ai_provider||"openai",smbcommerce_telegram_chat_id:c.settings.smbcommerce_telegram_chat_id||"",smbcommerce_sales_playbook:c.settings.smbcommerce_sales_playbook||"",smbcommerce_business_info:c.settings.smbcommerce_business_info||"",smbcommerce_enable_adaptive_playbook:c.settings.smbcommerce_enable_adaptive_playbook||"1",smbcommerce_meta_prompt_template:c.settings.smbcommerce_meta_prompt_template||"",smbcommerce_enable_prompt_creator:c.settings.smbcommerce_enable_prompt_creator||"1",smbcommerce_prompt_creator_template:c.settings.smbcommerce_prompt_creator_template||"",smbcommerce_max_upload_size:c.settings.smbcommerce_max_upload_size||"5",smbcommerce_handoff_webhook:c.settings.smbcommerce_handoff_webhook||"",smbcommerce_system_guardrails_prompt:c.settings.smbcommerce_system_guardrails_prompt||"",smbcommerce_media_retention_days:c.settings.smbcommerce_media_retention_days||"30",smbcommerce_widget_slogan:c.settings.smbcommerce_widget_slogan||"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:c.settings.smbcommerce_suggestion_chips||"",smbcommerce_hotline:c.settings.smbcommerce_hotline||"",smbcommerce_zalo_link:c.settings.smbcommerce_zalo_link||"",smbcommerce_messenger_link:c.settings.smbcommerce_messenger_link||"",smbcommerce_whatsapp:c.settings.smbcommerce_whatsapp||"",smbcommerce_telegram_link:c.settings.smbcommerce_telegram_link||"",smbcommerce_ai_notice_enabled:c.settings.smbcommerce_ai_notice_enabled??"1",smbcommerce_ai_notice_text:c.settings.smbcommerce_ai_notice_text||"",smbcommerce_hide_on_checkout:c.settings.smbcommerce_hide_on_checkout??"0",smbcommerce_voice_input_enabled:c.settings.smbcommerce_voice_input_enabled??"0",smbcommerce_gemini_model:c.settings.smbcommerce_gemini_model||"",smbcommerce_anthropic_model:c.settings.smbcommerce_anthropic_model||"",smbcommerce_openai_model:c.settings.smbcommerce_openai_model||"",smbcommerce_refund_policy:c.settings.smbcommerce_refund_policy||"",smbcommerce_warranty_policy:c.settings.smbcommerce_warranty_policy||"",smbcommerce_delivery_policy:c.settings.smbcommerce_delivery_policy||"",smbcommerce_privacy_policy:c.settings.smbcommerce_privacy_policy||"",smbcommerce_enable_link_reading:c.settings.smbcommerce_enable_link_reading||"0",smbcommerce_budget_daily_usd:c.settings.smbcommerce_budget_daily_usd||"0",smbcommerce_budget_monthly_usd:c.settings.smbcommerce_budget_monthly_usd||"0",smbcommerce_enable_provider_fallback:c.settings.smbcommerce_enable_provider_fallback||"1",smbcommerce_block_ungrounded_prices:c.settings.smbcommerce_block_ungrounded_prices||"0",smbcommerce_enable_response_cache:c.settings.smbcommerce_enable_response_cache||"0",smbcommerce_cache_ttl_hours:c.settings.smbcommerce_cache_ttl_hours||"24",smbcommerce_webhook_url:c.settings.smbcommerce_webhook_url||"",smbcommerce_webhook_secret:""})),q(c.settings.smbcommerce_license||null),Z(c.settings.smbcommerce_features||{}),G(c.settings.smbcommerce_premium_build!==!1),m(c.settings.smbcommerce_model_report||null),T(c.settings.smbcommerce_business_info_is_sample?c.settings.smbcommerce_business_info:null),o({openai:c.settings.smbcommerce_openai_key_exists,anthropic:c.settings.smbcommerce_anthropic_key_exists,gemini:c.settings.smbcommerce_gemini_key_exists,telegram:c.settings.smbcommerce_telegram_bot_token_exists}))}catch(n){console.error("Failed to load settings",n)}},Fe=async n=>{n.preventDefault(),i(!0),b("");try{const c={smbcommerce_brand_name:t.smbcommerce_brand_name,smbcommerce_primary_color:t.smbcommerce_primary_color,smbcommerce_logo_url:t.smbcommerce_logo_url,smbcommerce_font_family:t.smbcommerce_font_family,smbcommerce_runtime_mode:t.smbcommerce_runtime_mode,smbcommerce_ai_provider:t.smbcommerce_ai_provider,smbcommerce_telegram_chat_id:t.smbcommerce_telegram_chat_id,smbcommerce_sales_playbook:t.smbcommerce_sales_playbook,smbcommerce_business_info:t.smbcommerce_business_info,smbcommerce_enable_adaptive_playbook:t.smbcommerce_enable_adaptive_playbook,smbcommerce_meta_prompt_template:t.smbcommerce_meta_prompt_template,smbcommerce_enable_prompt_creator:t.smbcommerce_enable_prompt_creator,smbcommerce_prompt_creator_template:t.smbcommerce_prompt_creator_template,smbcommerce_max_upload_size:t.smbcommerce_max_upload_size,smbcommerce_handoff_webhook:t.smbcommerce_handoff_webhook,smbcommerce_system_guardrails_prompt:t.smbcommerce_system_guardrails_prompt,smbcommerce_media_retention_days:t.smbcommerce_media_retention_days,smbcommerce_widget_slogan:t.smbcommerce_widget_slogan,smbcommerce_suggestion_chips:t.smbcommerce_suggestion_chips,smbcommerce_hotline:t.smbcommerce_hotline,smbcommerce_zalo_link:t.smbcommerce_zalo_link,smbcommerce_messenger_link:t.smbcommerce_messenger_link,smbcommerce_whatsapp:t.smbcommerce_whatsapp,smbcommerce_telegram_link:t.smbcommerce_telegram_link,smbcommerce_ai_notice_enabled:t.smbcommerce_ai_notice_enabled,smbcommerce_ai_notice_text:t.smbcommerce_ai_notice_text,smbcommerce_hide_on_checkout:t.smbcommerce_hide_on_checkout,smbcommerce_voice_input_enabled:t.smbcommerce_voice_input_enabled,smbcommerce_gemini_model:t.smbcommerce_gemini_model,smbcommerce_anthropic_model:t.smbcommerce_anthropic_model,smbcommerce_openai_model:t.smbcommerce_openai_model,smbcommerce_refund_policy:t.smbcommerce_refund_policy,smbcommerce_warranty_policy:t.smbcommerce_warranty_policy,smbcommerce_delivery_policy:t.smbcommerce_delivery_policy,smbcommerce_privacy_policy:t.smbcommerce_privacy_policy,smbcommerce_enable_link_reading:t.smbcommerce_enable_link_reading,smbcommerce_budget_daily_usd:t.smbcommerce_budget_daily_usd,smbcommerce_budget_monthly_usd:t.smbcommerce_budget_monthly_usd,smbcommerce_enable_provider_fallback:t.smbcommerce_enable_provider_fallback,smbcommerce_block_ungrounded_prices:t.smbcommerce_block_ungrounded_prices,smbcommerce_enable_response_cache:t.smbcommerce_enable_response_cache,smbcommerce_cache_ttl_hours:t.smbcommerce_cache_ttl_hours,smbcommerce_webhook_url:t.smbcommerce_webhook_url};t.smbcommerce_webhook_secret&&(c.smbcommerce_webhook_secret=t.smbcommerce_webhook_secret),t.smbcommerce_openai_key&&(c.smbcommerce_openai_key=t.smbcommerce_openai_key),t.smbcommerce_anthropic_key&&(c.smbcommerce_anthropic_key=t.smbcommerce_anthropic_key),t.smbcommerce_gemini_key&&(c.smbcommerce_gemini_key=t.smbcommerce_gemini_key),t.smbcommerce_telegram_bot_token&&(c.smbcommerce_telegram_bot_token=t.smbcommerce_telegram_bot_token);const L=await fetch(`${x}/admin/settings`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify(c)});if(!L.ok){const X=await L.json().catch(()=>null);throw new Error((X==null?void 0:X.message)||"Save failed")}b("Settings saved successfully!"),s(X=>({...X,smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:""})),de()}catch(c){const L=c instanceof Error&&c.message!=="Save failed"?` ${c.message}`:"";b(`Error saving settings.${L}`),console.error(c)}finally{i(!1),setTimeout(()=>b(""),8e3)}},ze=()=>{r.ask("Restore the default playbook? Your current text in this field will be replaced.","Restore default",je)},je=async()=>{try{const c=await(await fetch(`${x}/admin/playbook/default`,{headers:N.getHeaders()})).json();s(L=>({...L,smbcommerce_sales_playbook:c.playbook||""})),b("Default playbook loaded. Click Save to apply."),setTimeout(()=>b(""),4e3)}catch{b("Could not load the default playbook.")}},Ue=async()=>{B(!0),D(null);try{const c=await(await fetch(`${x}/admin/products/sync`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})})).json();D(c),he()}catch(n){D({error:n.message})}finally{B(!1)}},he=async()=>{try{const n=await fetch(`${x}/admin/products/status`,{headers:N.getHeaders()});u(await n.json())}catch{}},Ve=async()=>{w(!0),E(null);try{const c=await(await fetch(`${x}/admin/ai-test`,{headers:N.getHeaders()})).json();E(c)}catch(n){E({error:n.message})}finally{w(!1)}},qe=()=>{const n=window.wp;if(!n||!n.media){b("The WordPress media library is not available on this page. Paste the logo URL instead."),setTimeout(()=>b(""),8e3);return}const c=n.media({title:"Select Logo",button:{text:"Use this logo"},multiple:!1});c.on("select",()=>{const L=c.state().get("selection").first().toJSON();s({...t,smbcommerce_logo_url:L.url})}),c.open()};return e("div",{className:"smb-commerce-settings-container",children:[f&&e("div",{className:"smb-commerce-notice",role:"status",children:e("p",{children:f})}),e(me,{request:r.request,onCancel:r.cancel}),e("div",{className:"smb-commerce-settings-layout",children:[e("div",{className:"smb-commerce-settings-form-col",children:e("form",{onSubmit:Fe,className:"smb-commerce-form",children:[e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-art"})," Widget appearance"]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Brand name"}),e("input",{type:"text",value:t.smbcommerce_brand_name,onChange:n=>s({...t,smbcommerce_brand_name:n.target.value})}),e("p",{className:"description",children:"The name shown in the chat window header."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Font"}),e("select",{value:t.smbcommerce_font_family,onChange:n=>s({...t,smbcommerce_font_family:n.target.value}),children:[e("option",{value:"system",children:"System font (no external requests)"}),e("option",{value:"Inter",children:"Inter (Google Fonts)"}),e("option",{value:"Roboto",children:"Roboto (Google Fonts)"}),e("option",{value:"Open Sans",children:"Open Sans (Google Fonts)"}),e("option",{value:"Montserrat",children:"Montserrat (Google Fonts)"}),e("option",{value:"Poppins",children:"Poppins (Google Fonts)"})]}),e("p",{className:"description",children:"Google Fonts are loaded from fonts.googleapis.com in your visitors' browsers."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Logo URL"}),e("div",{style:{display:"flex",gap:"8px"},children:[e("input",{type:"text",value:t.smbcommerce_logo_url,onChange:n=>s({...t,smbcommerce_logo_url:n.target.value}),placeholder:"https://...",style:{flex:1}}),e("button",{type:"button",className:"button",onClick:qe,children:"Upload image"})]}),e("p",{className:"description",children:"Shown on the left of the widget header."})]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Primary colour"}),e("div",{className:"color-picker-wrapper",children:[e("input",{type:"color",value:t.smbcommerce_primary_color,onChange:n=>s({...t,smbcommerce_primary_color:n.target.value})}),e("span",{className:"color-hex",children:t.smbcommerce_primary_color})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Footer tagline"}),e("input",{type:"text",placeholder:"AI sales assistant, available 24/7",value:t.smbcommerce_widget_slogan,onChange:n=>s({...t,smbcommerce_widget_slogan:n.target.value})})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Other ways to reach you"}),e("p",{className:"description",children:"Shown as small icons under the chat box. Leave a field empty to hide that channel."}),e("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"10px",marginTop:"5px"},children:[e("input",{type:"text","aria-label":"WhatsApp number or link",placeholder:"WhatsApp number (e.g. +1 555 0100)",value:t.smbcommerce_whatsapp||"",onChange:n=>s({...t,smbcommerce_whatsapp:n.target.value})}),e("input",{type:"text","aria-label":"Messenger link",placeholder:"Messenger link (e.g. https://m.me/...)",value:t.smbcommerce_messenger_link||"",onChange:n=>s({...t,smbcommerce_messenger_link:n.target.value})}),e("input",{type:"text","aria-label":"Telegram username or link",placeholder:"Telegram (e.g. @yourshop)",value:t.smbcommerce_telegram_link||"",onChange:n=>s({...t,smbcommerce_telegram_link:n.target.value})}),e("input",{type:"text","aria-label":"Zalo link",placeholder:"Zalo link (e.g. https://zalo.me/...)",value:t.smbcommerce_zalo_link||"",onChange:n=>s({...t,smbcommerce_zalo_link:n.target.value})}),e("input",{type:"text","aria-label":"Phone number",placeholder:"Phone (e.g. +1 555 0100)",value:t.smbcommerce_hotline||"",onChange:n=>s({...t,smbcommerce_hotline:n.target.value})})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_ai_notice_enabled==="1",onChange:n=>s({...t,smbcommerce_ai_notice_enabled:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Tell visitors they are chatting with AI"}),e("p",{className:"description",style:{margin:0},children:"Shows a short line at the top of the chat. Recommended, and expected by AI transparency rules in many countries."})]})]}),t.smbcommerce_ai_notice_enabled==="1"&&e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-ai-notice",children:"AI notice text"}),e("input",{id:"smb-ai-notice",type:"text",maxLength:200,placeholder:"You are chatting with an AI assistant. A team member can join when needed.",value:t.smbcommerce_ai_notice_text,onChange:n=>s({...t,smbcommerce_ai_notice_text:n.target.value})}),e("p",{className:"description",children:"Leave empty to use the default text, translated into the site language."})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_hide_on_checkout==="1",onChange:n=>s({...t,smbcommerce_hide_on_checkout:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Hide the chat on the checkout page"}),e("p",{className:"description",style:{margin:0},children:"Off by default. The chat button already moves up on cart and checkout pages so it does not cover the order button."})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_voice_input_enabled==="1",onChange:n=>s({...t,smbcommerce_voice_input_enabled:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let visitors speak their message"}),e("p",{className:"description",style:{margin:0},children:"Adds a microphone button. The visitor's browser turns speech into text in the message box, and the visitor checks it before sending. The plugin never receives or stores audio, but depending on the browser the audio may be sent to the browser maker's speech service (for example Google for Chrome, Apple for Safari). Shown only on HTTPS pages in browsers that support it (not Firefox). Off by default."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Suggested question chips"}),e("textarea",{style:{width:"100%",height:"80px",padding:"12px"},placeholder:`One per line. For example:
     1import{u as e,S as D,d,A as Z,h as F}from"./assets/hooks.module-C33f2O-M.js";import{p as $e,n as Be,u as oe,a as ce,b as w,N as ie,C as le,B as H,t as be}from"./assets/i18n-D_zElFbw.js";function Ne(r){return{render:function(t){Be(t,r)},unmount:function(){$e(r)}}}function _e({card:r}){const t=r.orders_capped?`${r.orders_count}+ orders`:`${r.orders_count} ${r.orders_count===1?"order":"orders"}`;return e("div",{className:"smb-customer-card",children:[e("h4",{children:r.name||(r.guest?"Guest customer":"Customer")}),r.other_customers>0&&e("p",{className:"smb-customer-warn",children:"Another customer was also verified in this chat. This card shows the most recent one."}),e("dl",{children:[e("dt",{children:"Orders"}),e("dd",{children:t}),e("dt",{children:"Total spent"}),e("dd",{children:r.total_spent.length?r.total_spent.map(n=>n.text).join(" · "):"No paid orders yet"}),r.last_order&&e(D,{children:[e("dt",{children:"Last order"}),e("dd",{children:[r.last_order.url?e("a",{href:r.last_order.url,children:["#",r.last_order.number]}):e(D,{children:["#",r.last_order.number]})," · ",r.last_order.status_label,e("div",{className:"smb-customer-sub",children:[r.last_order.date," · ",r.last_order.total_text]})]})]}),r.recent_products.length>0&&e(D,{children:[e("dt",{children:"Recently bought"}),e("dd",{children:e("ul",{children:r.recent_products.map((n,p)=>e("li",{children:n.url?e("a",{href:n.url,target:"_blank",rel:"noopener noreferrer",children:n.name}):n.name},p))})})]})]}),(r.links.orders||r.links.profile)&&e("p",{className:"smb-customer-links",children:[r.links.orders&&e("a",{href:r.links.orders,children:"All orders"}),r.links.orders&&r.links.profile&&" · ",r.links.profile&&e("a",{href:r.links.profile,children:"Customer profile"})]}),e("p",{className:"smb-customer-note",children:[r.guest?"Guest checkout: only the order verified in this chat is shown. ":"","Read-only, from WooCommerce. Not shared with the AI."]})]})}const Oe={bot_active:"AI assistant",human_active:"Waiting for your team",awaiting_agent:"Waiting for your team",agent_active:"Team member replying",closed:"Closed"},Pe={webchat:"Website chat"},ge=r=>Oe[r]||r,We=r=>r.sender_type==="user"?"Customer":r.sender_type==="agent"?r.first_name?`${r.first_name} (team)`:"Team member":r.sender_type==="system"?"Assistant":r.sender_type;function De({openId:r=null}={}){const{notices:t,push:n,dismiss:p}=oe(),m=ce(),[u,k]=d([]),[h,g]=d(null),[x,T]=d([]),[_,N]=d(""),[C,I]=d(new Set),[L,$]=d(!1),v=Z(null),i=Z(5e3),l=Z(null),R=Z(null),[B,P]=d(null),O=Z(null),M=o=>{w.get(`/admin/conversations/${o}/customer`).then(f=>{O.current===o&&P((f==null?void 0:f.card)||null)}).catch(()=>{O.current===o&&P(null)})};F(()=>{S()},[]),F(()=>{r&&w.get(`/admin/conversations/${r}`).then(o=>o&&U(o)).catch(()=>n("error","This conversation no longer exists."))},[r]);const S=async()=>{try{const o=await w.getConversations();k(o.items||[])}catch(o){console.error(o)}},U=async o=>{var f;g(o),O.current=o.id,P(null),M(o.id);try{const y=await w.getMessages(o.id);T(y.messages||[]),R.current=((f=y.messages)==null?void 0:f.length)>0?y.messages[y.messages.length-1].id:null,q()}catch(y){console.error(y)}},q=()=>setTimeout(()=>{var o;return(o=v.current)==null?void 0:o.scrollIntoView({behavior:"smooth"})},100),G=async()=>{var o;if(!h||document.visibilityState!=="visible"){l.current=window.setTimeout(G,i.current);return}try{const f=await w.getMessages(h.id,R.current);((o=f.messages)==null?void 0:o.length)>0&&(T(y=>{const A=new Set(f.messages.map(z=>z.id));return[...y.filter(z=>!A.has(z.id)),...f.messages]}),R.current=f.messages[f.messages.length-1].id,q(),i.current=5e3,M(h.id))}catch{i.current=Math.min(i.current*2,6e4)}l.current=window.setTimeout(G,i.current)};F(()=>(h&&(l.current=window.setTimeout(G,i.current)),()=>{l.current&&clearTimeout(l.current)}),[h]);const b=async()=>{if(h)try{await w.takeover(h.id),g({...h,status:"agent_active"}),S()}catch{n("error","Takeover failed. Another agent may already have this conversation.")}},J=async()=>{if(h)try{await w.release(h.id),g({...h,status:"bot_active"}),S()}catch{n("error","Could not hand the conversation back to the bot.")}},K=(o,f)=>{f.stopPropagation(),m.ask(`Permanently delete conversation #${o}? This cannot be undone.`,"Delete",async()=>{try{await w.deleteConversation(o),(h==null?void 0:h.id)===o&&(g(null),T([]),P(null),O.current=null),I(y=>{const A=new Set(y);return A.delete(o),A}),S(),n("success",`Conversation #${o} deleted.`)}catch{n("error","Could not delete the conversation.")}})},te=(o,f)=>{f.stopPropagation(),I(y=>{const A=new Set(y);return A.has(o)?A.delete(o):A.add(o),A})},se=()=>{C.size===u.length&&u.length>0?I(new Set):I(new Set(u.map(o=>o.id)))},re=()=>{if(C.size===0)return;const o=C.size;m.ask(`Permanently delete ${o} selected conversation(s)? This cannot be undone.`,`Delete ${o}`,async()=>{$(!0);try{const f=await w.bulkDeleteConversations(Array.from(C));h&&C.has(h.id)&&(g(null),T([]),P(null),O.current=null),I(new Set),S(),f.failed>0?n("warning",`${f.failed} conversation(s) could not be deleted.`):n("success",`${o} conversation(s) deleted.`)}catch{n("error","Bulk delete failed.")}finally{$(!1)}})},W=async()=>{if(!_.trim()||!h)return;const o=_.trim();N("");const f=crypto.randomUUID(),y={id:-Date.now(),sender_type:"agent",content:o,created_at:new Date().toISOString()};T(A=>[...A,y]),q();try{const A=await w.sendMessage(h.id,o,f);T(z=>z.map(X=>X.id===y.id?A:X)),R.current=A.id}catch{n("error","Could not send the message. Your text is back in the reply box."),T(A=>A.filter(z=>z.id!==y.id)),N(o)}},Q={bot_active:"#10b981",human_active:"#f59e0b",agent_active:"#3b82f6",closed:"#9ca3af"};return e(D,{children:[e(ie,{notices:t,onDismiss:p}),e(le,{request:m.request,onCancel:m.cancel}),e("div",{className:"smb-commerce-inbox-container",children:[e("div",{className:"smb-commerce-sidebar",children:[e("div",{style:{padding:"12px 15px",borderBottom:"1px solid #ccd0d4",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("h2",{style:{margin:0,fontSize:"15px",fontWeight:700},children:["Conversations (",u.length,")"]}),e("button",{className:"button",style:{fontSize:"12px",padding:"3px 8px"},onClick:se,children:C.size===u.length&&u.length>0?"Deselect all":"Select all"})]}),C.size>0&&e("div",{style:{padding:"8px 12px",background:"#fff3cd",borderBottom:"1px solid #f0c36d",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("span",{style:{fontSize:"13px",fontWeight:600,color:"#856404"},children:["Selected: ",C.size]}),e("button",{style:{background:"#d63638",color:"white",border:"none",fontSize:"12px",padding:"4px 12px",borderRadius:"4px",cursor:"pointer",fontWeight:600},onClick:re,disabled:L,children:L?"Deleting…":"Delete "+C.size})]}),e("ul",{className:"smb-commerce-conversation-list",style:{listStyle:"none",margin:0,padding:0},children:u.map(o=>e("li",{className:"smb-commerce-conversation-item "+((h==null?void 0:h.id)===o.id?"active":""),onClick:()=>U(o),style:{display:"flex",alignItems:"center",gap:"8px",padding:"10px 12px",background:C.has(o.id)?"#eff6ff":void 0},children:[e("input",{type:"checkbox",checked:C.has(o.id),onClick:f=>te(o.id,f),onChange:()=>{},style:{width:"15px",height:"15px",flexShrink:0,cursor:"pointer"}}),e("div",{style:{flex:1,minWidth:0},children:[e("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"4px"},children:[e("span",{style:{fontWeight:600,fontSize:"13px"},children:["Conversation #",o.id]}),e("span",{style:{fontSize:"10px",fontWeight:600,color:"white",background:Q[o.status]||"#9ca3af",padding:"1px 6px",borderRadius:"10px",whiteSpace:"nowrap"},children:ge(o.status)})]}),e("div",{style:{fontSize:"11px",color:"#666",marginTop:"2px"},children:Pe[o.channel]||o.channel})]}),e("button",{style:{color:"#d63638",padding:"2px",background:"none",border:"none",cursor:"pointer",flexShrink:0},onClick:f=>K(o.id,f),title:"Delete conversation","aria-label":`Delete conversation #${o.id}`,children:e("span",{className:"dashicons dashicons-trash",style:{fontSize:"16px"}})})]},o.id))})]}),e("div",{className:"smb-commerce-main",children:h?e(D,{children:[e("div",{className:"smb-commerce-main-header",children:[e("h3",{children:["Conversation #",h.id," — ",e("span",{style:{color:Q[h.status]},children:ge(h.status)})]}),e("div",{className:"actions",children:[h.status!=="agent_active"&&e("button",{onClick:b,className:"button button-primary",children:"Take over"}),h.status==="agent_active"&&e("button",{onClick:J,className:"button button-secondary",children:"Hand back to bot"})]})]}),B&&e("details",{className:"smb-customer-inline",children:[e("summary",{children:["Customer: ",B.name||"Guest customer"]}),e(_e,{card:B})]}),e("div",{className:"smb-commerce-chat-history",children:[x.map(o=>e("div",{className:"smb-commerce-admin-msg "+o.sender_type,children:[e("div",{className:"msg-sender",children:We(o)}),e("div",{className:"msg-content",children:o.content})]},o.id)),e("div",{ref:v})]}),e("div",{className:"smb-commerce-main-input",children:[e("input",{type:"text",value:_,onInput:o=>N(o.target.value),onKeyPress:o=>o.key==="Enter"&&W(),placeholder:"Type your reply…",disabled:h.status!=="agent_active"&&h.status!=="awaiting_agent"}),e("button",{className:"button button-primary",onClick:W,disabled:h.status!=="agent_active"&&h.status!=="awaiting_agent",children:"Send"})]})]}):e("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",color:"#666"},children:"Select a conversation to get started"})}),h&&B&&e("aside",{className:"smb-customer-aside","aria-label":"Customer",children:e(_e,{card:B})})]})]})}const Fe={"message.created":"Visitor sends a message","conversation.created":"Visitor opens the chat"},Me={content:"Message text",time:"Site hour"},ye={soft:"normal priority",hard:"high priority",emergency:"urgent"},xe=r=>{if(typeof r!="string")return r||{};try{return JSON.parse(r)||{}}catch{return{}}},ze=r=>{const t=xe(r),n=t.conditions;if(!Array.isArray(n)||n.length===0)return"Always";const p=m=>String(m??"").split(",").map(u=>u.trim()).filter(Boolean).map(u=>`"${u}"`).join(" or ");return n.map(m=>{if(m.type==="message_contains")return`Message text contains "${m.value}"`;if(m.type==="conversation.has_tag")return`Conversation has the tag "${m.value}"`;const u=Me[m.field]||m.field;return m.operator==="contains"?`${u} contains ${p(m.value)}`:m.operator==="equals"?`${u} is exactly "${m.value}"`:m.operator==="after"?`${u} is ${m.value}:00 or later`:JSON.stringify(m)}).join(t.type==="or"?" or ":" and ")},He=r=>{const t=xe(r).actions;return!Array.isArray(t)||t.length===0?JSON.stringify(r):t.map(n=>n.type==="handoff"?`Hand off to a person (${ye[String(n.value||"").toLowerCase()]||ye.soft})`:n.type==="send_message"?`Reply: "${n.value}"`:n.type==="tag_conversation"?`Add the tag "${n.value}"`:JSON.stringify(n)).join(", then ")},je=()=>{const{notices:r,push:t,dismiss:n}=oe(),p=ce(),[m,u]=d(""),[k,h]=d([]),[g,x]=d(!0),[T,_]=d(!1),[N,C]=d(""),[I,L]=d("message.created"),[$,v]=d("content"),[i,l]=d("contains"),[R,B]=d(""),[P,O]=d("handoff"),[M,S]=d("Soft"),U=async()=>{x(!0);try{const b=await w.get("/admin/rules");b.items&&h(b.items)}catch(b){console.error("Failed to load rules",b)}x(!1)};F(()=>{U()},[]);const q=(b,J)=>{p.ask(`Delete the rule "${J}"?`,"Delete rule",async()=>{try{await w.delete(`/admin/rules/${b}`),U(),t("success","Rule deleted.")}catch{t("error","Could not delete the rule.")}})},G=async()=>{if(!N.trim()){u("Please enter a rule name.");return}u("");const b={name:N,trigger_event:I,conditions:{type:"and",conditions:[{type:"simple",field:$,operator:i,value:R}]},actions:{actions:[{type:P,value:M}]},priority:10,is_active:1};try{await w.post("/admin/rules",b),_(!1),C(""),B(""),U(),t("success","Rule saved and enabled.")}catch{t("error","Could not save the rule.")}};return g?e("div",{role:"status",children:"Loading…"}):e("div",{className:"smb-commerce-rules",children:[e(ie,{notices:r,onDismiss:n}),e(le,{request:p.request,onCancel:p.cancel}),e("h2",{children:"Automation rules"}),e("p",{children:"Automatically run an action when a visitor sends a message or opens the chat, based on conditions you define."}),T?e("div",{className:"rule-form",style:{background:"#fff",padding:"20px",border:"1px solid #ccd0d4",borderRadius:"4px",maxWidth:"800px"},children:[e("h3",{style:{marginTop:0},children:"Create an automation rule"}),e("div",{style:{marginBottom:15},children:[e("label",{htmlFor:"smb-rule-name",style:{fontWeight:600},children:"Rule name:"}),e("br",{}),e("input",{id:"smb-rule-name",type:"text",className:"regular-text",style:{width:"100%",maxWidth:"400px"},value:N,onChange:b=>{C(b.target.value),m&&u("")},"aria-invalid":m?!0:void 0,"aria-describedby":m?"smb-rule-name-error":void 0,placeholder:"e.g. Hand off when a visitor complains"}),m&&e("p",{id:"smb-rule-name-error",className:"smb-field-error",role:"alert",children:m})]}),e("div",{style:{marginBottom:15},children:[e("label",{style:{fontWeight:600},children:"1. When this happens (trigger):"}),e("br",{}),e("select",{value:I,onChange:b=>L(b.target.value),style:{width:"100%",maxWidth:"400px"},children:[e("option",{value:"message.created",children:"Visitor sends a new message"}),e("option",{value:"conversation.created",children:"Visitor opens the chat for the first time"})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #2271b1"},children:[e("label",{style:{fontWeight:600},children:"2. AND these conditions match (if):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:$,onChange:b=>v(b.target.value),children:[e("option",{value:"content",children:"Message text"}),e("option",{value:"time",children:"Current time (site hour, 0–23)"})]}),e("select",{value:i,onChange:b=>l(b.target.value),children:[e("option",{value:"contains",children:"Contains keyword"}),e("option",{value:"equals",children:"Exactly matches"}),e("option",{value:"after",children:"After (hour)"})]}),e("input",{type:"text",value:R,onChange:b=>B(b.target.value),placeholder:"e.g. refund, quote..."})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #00a32a"},children:[e("label",{style:{fontWeight:600},children:"3. THEN do this (then):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:P,onChange:b=>O(b.target.value),children:[e("option",{value:"handoff",children:"Hand off to a human agent"}),e("option",{value:"send_message",children:"Send an automatic message"}),e("option",{value:"tag_conversation",children:"Add a tag"})]}),e("input",{type:"text",value:M,onChange:b=>S(b.target.value),placeholder:"e.g. Soft (or the message text)",style:{flex:1}})]})]}),e("div",{style:{marginTop:20,paddingTop:"15px",borderTop:"1px solid #ddd"},children:[e("button",{onClick:G,className:"button button-primary",children:"Save and enable"}),e("button",{onClick:()=>_(!1),className:"button",style:{marginLeft:10},children:"Cancel"})]})]}):e(D,{children:[e("button",{onClick:()=>_(!0),className:"button button-primary",style:{marginBottom:15},children:"+ New rule"}),e("table",{className:"wp-list-table widefat fixed striped",children:[e("thead",{children:e("tr",{children:[e("th",{children:"Rule name"}),e("th",{children:"When"}),e("th",{children:"If"}),e("th",{children:"Then"}),e("th",{children:"Status"}),e("th",{children:"Actions"})]})}),e("tbody",{children:[k.map(b=>e("tr",{children:[e("td",{children:e("strong",{children:b.name})}),e("td",{children:Fe[b.trigger_event]||b.trigger_event}),e("td",{children:ze(b.conditions)}),e("td",{children:He(b.actions)}),e("td",{children:b.is_active?e("span",{style:{color:"green"},children:"Active"}):e("span",{style:{color:"gray"},children:"Disabled"})}),e("td",{children:e("button",{type:"button",onClick:()=>q(b.id,b.name),className:"button-link button-link-delete",children:"Delete"})})]},b.id)),k.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No rules created yet."})})]})]})]})]})},Ue=()=>{const[r,t]=d([]),[n,p]=d(!0),m=async()=>{p(!0);try{const k=await w.getLeads();k.items&&t(k.items)}catch(k){console.error("Failed to load leads",k)}p(!1)};return F(()=>{m()},[]),n?e("div",{children:"Loading leads..."}):e("div",{className:"smb-commerce-leads",children:[e("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e("h2",{children:"Leads"}),e("button",{className:"button button-primary",onClick:()=>{var h,g;const k=`${((h=window.SMBCommerceAdmin)==null?void 0:h.restUrl)||"/wp-json/smb-commerce/v1"}/admin/leads/export?_wpnonce=${((g=window.SMBCommerceAdmin)==null?void 0:g.nonce)||""}`;window.location.href=k},children:[e("span",{className:"dashicons dashicons-download",style:{marginTop:"4px"}})," Export CSV"]})]}),e("table",{className:"wp-list-table widefat fixed striped",style:{marginTop:"10px"},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Name"}),e("th",{children:"Email"}),e("th",{children:"Phone"}),e("th",{children:"Score"}),e("th",{children:"Status"}),e("th",{children:"Created At"})]})}),e("tbody",{children:[r.map(k=>e("tr",{children:[e("td",{children:k.name||"-"}),e("td",{children:k.email||"-"}),e("td",{children:k.phone||"-"}),e("td",{children:k.score}),e("td",{children:k.status}),e("td",{children:new Date(k.created_at).toLocaleString()})]},k.id)),r.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No leads found."})})]})]})]})};function qe(){var he,ue,pe;const r=ce(),[t,n]=d({smbcommerce_brand_name:"Shop Assistant",smbcommerce_primary_color:"#01696f",smbcommerce_logo_url:"",smbcommerce_font_family:"system",smbcommerce_runtime_mode:"smart_hybrid",smbcommerce_ai_provider:"openai",smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:"",smbcommerce_telegram_chat_id:"",smbcommerce_sales_playbook:"",smbcommerce_business_info:"",smbcommerce_enable_adaptive_playbook:"1",smbcommerce_meta_prompt_template:"",smbcommerce_enable_prompt_creator:"1",smbcommerce_prompt_creator_template:"",smbcommerce_max_upload_size:"5",smbcommerce_handoff_webhook:"",smbcommerce_system_guardrails_prompt:"",smbcommerce_media_retention_days:"30",smbcommerce_widget_slogan:"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:"",smbcommerce_hotline:"",smbcommerce_zalo_link:"",smbcommerce_messenger_link:"",smbcommerce_whatsapp:"",smbcommerce_telegram_link:"",smbcommerce_ai_notice_enabled:"1",smbcommerce_ai_notice_text:"",smbcommerce_hide_on_checkout:"0",smbcommerce_voice_input_enabled:"0",smbcommerce_reply_language:"customer",smbcommerce_coupon_allowlist:"",smbcommerce_tracking_code_meta:"",smbcommerce_tracking_carrier_meta:"",smbcommerce_tracking_carrier:"",smbcommerce_tracking_url_template:"",smbcommerce_abilities_enabled:"0",smbcommerce_gemini_model:"",smbcommerce_anthropic_model:"",smbcommerce_openai_model:"",smbcommerce_refund_policy:"",smbcommerce_warranty_policy:"",smbcommerce_delivery_policy:"",smbcommerce_privacy_policy:"",smbcommerce_enable_link_reading:"0",smbcommerce_budget_daily_usd:"0",smbcommerce_budget_monthly_usd:"0",smbcommerce_enable_provider_fallback:"1",smbcommerce_block_ungrounded_prices:"0",smbcommerce_enable_response_cache:"0",smbcommerce_cache_ttl_hours:"24",smbcommerce_webhook_url:"",smbcommerce_webhook_secret:""}),[p,m]=d({openai:!1,anthropic:!1,gemini:!1,telegram:!1}),[u,k]=d(!1),[h,g]=d(""),[x,T]=d(null),[_,N]=d(!1),[C,I]=d(!1),[L,$]=d(null),[v,i]=d(null),[l,R]=d(null),[B,P]=d(30),[O,M]=d(null),[S,U]=d(null),[q,G]=d({}),[b,J]=d(!0),[K,te]=d(!1),[se,re]=d(!1),[W,Q]=d(null),[o,f]=d(null),y=s=>!q[s]||q[s].allowed;F(()=>{me(),de()},[]),F(()=>{A(B)},[B]);const A=async s=>{try{const a=await fetch(`${H}/admin/analytics?days=${s}`,{headers:w.getHeaders()});R(await a.json())}catch(a){console.error("Analytics load failed",a)}},z=async()=>{M({pending:!0});try{const s=await fetch(`${H}/admin/webhook/test`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})});M(await s.json())}catch(s){M({message:s.message})}},X=async()=>{try{await fetch(`${H}/admin/cache/flush`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})}),A(B),g("Cache cleared."),setTimeout(()=>g(""),3e3)}catch{g("Could not clear the cache.")}},V=s=>"$"+(Number(s)||0).toFixed(s<1?4:2);F(()=>{const s=t.smbcommerce_font_family;if(!s||s==="system")return;const a=`smb-commerce-font-${s.replace(/\s+/g,"-")}`;if(!document.getElementById(a)){const E=document.createElement("link");E.id=a,E.rel="stylesheet",E.href=`https://fonts.googleapis.com/css2?family=${s.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(E)}},[t.smbcommerce_font_family]);const me=async()=>{try{const a=await(await fetch(`${H}/admin/settings`,{headers:w.getHeaders()})).json();a.settings&&(n(E=>({...E,smbcommerce_brand_name:a.settings.smbcommerce_brand_name||"",smbcommerce_primary_color:a.settings.smbcommerce_primary_color||"",smbcommerce_logo_url:a.settings.smbcommerce_logo_url||"",smbcommerce_font_family:a.settings.smbcommerce_font_family||"system",smbcommerce_runtime_mode:a.settings.smbcommerce_runtime_mode||"smart_hybrid",smbcommerce_ai_provider:a.settings.smbcommerce_ai_provider||"openai",smbcommerce_telegram_chat_id:a.settings.smbcommerce_telegram_chat_id||"",smbcommerce_sales_playbook:a.settings.smbcommerce_sales_playbook||"",smbcommerce_business_info:a.settings.smbcommerce_business_info||"",smbcommerce_enable_adaptive_playbook:a.settings.smbcommerce_enable_adaptive_playbook||"1",smbcommerce_meta_prompt_template:a.settings.smbcommerce_meta_prompt_template||"",smbcommerce_enable_prompt_creator:a.settings.smbcommerce_enable_prompt_creator||"1",smbcommerce_prompt_creator_template:a.settings.smbcommerce_prompt_creator_template||"",smbcommerce_max_upload_size:a.settings.smbcommerce_max_upload_size||"5",smbcommerce_handoff_webhook:a.settings.smbcommerce_handoff_webhook||"",smbcommerce_system_guardrails_prompt:a.settings.smbcommerce_system_guardrails_prompt||"",smbcommerce_media_retention_days:a.settings.smbcommerce_media_retention_days||"30",smbcommerce_widget_slogan:a.settings.smbcommerce_widget_slogan||"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:a.settings.smbcommerce_suggestion_chips||"",smbcommerce_hotline:a.settings.smbcommerce_hotline||"",smbcommerce_zalo_link:a.settings.smbcommerce_zalo_link||"",smbcommerce_messenger_link:a.settings.smbcommerce_messenger_link||"",smbcommerce_whatsapp:a.settings.smbcommerce_whatsapp||"",smbcommerce_telegram_link:a.settings.smbcommerce_telegram_link||"",smbcommerce_ai_notice_enabled:a.settings.smbcommerce_ai_notice_enabled??"1",smbcommerce_ai_notice_text:a.settings.smbcommerce_ai_notice_text||"",smbcommerce_hide_on_checkout:a.settings.smbcommerce_hide_on_checkout??"0",smbcommerce_voice_input_enabled:a.settings.smbcommerce_voice_input_enabled??"0",smbcommerce_reply_language:a.settings.smbcommerce_reply_language||"customer",smbcommerce_coupon_allowlist:a.settings.smbcommerce_coupon_allowlist||"",smbcommerce_tracking_code_meta:a.settings.smbcommerce_tracking_code_meta||"",smbcommerce_tracking_carrier_meta:a.settings.smbcommerce_tracking_carrier_meta||"",smbcommerce_tracking_carrier:a.settings.smbcommerce_tracking_carrier||"",smbcommerce_tracking_url_template:a.settings.smbcommerce_tracking_url_template||"",smbcommerce_abilities_enabled:a.settings.smbcommerce_abilities_enabled??"0",smbcommerce_gemini_model:a.settings.smbcommerce_gemini_model||"",smbcommerce_anthropic_model:a.settings.smbcommerce_anthropic_model||"",smbcommerce_openai_model:a.settings.smbcommerce_openai_model||"",smbcommerce_refund_policy:a.settings.smbcommerce_refund_policy||"",smbcommerce_warranty_policy:a.settings.smbcommerce_warranty_policy||"",smbcommerce_delivery_policy:a.settings.smbcommerce_delivery_policy||"",smbcommerce_privacy_policy:a.settings.smbcommerce_privacy_policy||"",smbcommerce_enable_link_reading:a.settings.smbcommerce_enable_link_reading||"0",smbcommerce_budget_daily_usd:a.settings.smbcommerce_budget_daily_usd||"0",smbcommerce_budget_monthly_usd:a.settings.smbcommerce_budget_monthly_usd||"0",smbcommerce_enable_provider_fallback:a.settings.smbcommerce_enable_provider_fallback||"1",smbcommerce_block_ungrounded_prices:a.settings.smbcommerce_block_ungrounded_prices||"0",smbcommerce_enable_response_cache:a.settings.smbcommerce_enable_response_cache||"0",smbcommerce_cache_ttl_hours:a.settings.smbcommerce_cache_ttl_hours||"24",smbcommerce_webhook_url:a.settings.smbcommerce_webhook_url||"",smbcommerce_webhook_secret:""})),U(a.settings.smbcommerce_license||null),G(a.settings.smbcommerce_features||{}),J(a.settings.smbcommerce_premium_build!==!1),te(a.settings.smbcommerce_wp_ai_available===!0),re(a.settings.smbcommerce_abilities_supported===!0),Q(a.settings.smbcommerce_model_report||null),f(a.settings.smbcommerce_business_info_is_sample?a.settings.smbcommerce_business_info:null),m({openai:a.settings.smbcommerce_openai_key_exists,anthropic:a.settings.smbcommerce_anthropic_key_exists,gemini:a.settings.smbcommerce_gemini_key_exists,telegram:a.settings.smbcommerce_telegram_bot_token_exists}))}catch(s){console.error("Failed to load settings",s)}},Ae=async s=>{s.preventDefault(),k(!0),g("");try{const a={smbcommerce_brand_name:t.smbcommerce_brand_name,smbcommerce_primary_color:t.smbcommerce_primary_color,smbcommerce_logo_url:t.smbcommerce_logo_url,smbcommerce_font_family:t.smbcommerce_font_family,smbcommerce_runtime_mode:t.smbcommerce_runtime_mode,smbcommerce_ai_provider:t.smbcommerce_ai_provider,smbcommerce_telegram_chat_id:t.smbcommerce_telegram_chat_id,smbcommerce_sales_playbook:t.smbcommerce_sales_playbook,smbcommerce_business_info:t.smbcommerce_business_info,smbcommerce_enable_adaptive_playbook:t.smbcommerce_enable_adaptive_playbook,smbcommerce_meta_prompt_template:t.smbcommerce_meta_prompt_template,smbcommerce_enable_prompt_creator:t.smbcommerce_enable_prompt_creator,smbcommerce_prompt_creator_template:t.smbcommerce_prompt_creator_template,smbcommerce_max_upload_size:t.smbcommerce_max_upload_size,smbcommerce_handoff_webhook:t.smbcommerce_handoff_webhook,smbcommerce_system_guardrails_prompt:t.smbcommerce_system_guardrails_prompt,smbcommerce_media_retention_days:t.smbcommerce_media_retention_days,smbcommerce_widget_slogan:t.smbcommerce_widget_slogan,smbcommerce_suggestion_chips:t.smbcommerce_suggestion_chips,smbcommerce_hotline:t.smbcommerce_hotline,smbcommerce_zalo_link:t.smbcommerce_zalo_link,smbcommerce_messenger_link:t.smbcommerce_messenger_link,smbcommerce_whatsapp:t.smbcommerce_whatsapp,smbcommerce_telegram_link:t.smbcommerce_telegram_link,smbcommerce_ai_notice_enabled:t.smbcommerce_ai_notice_enabled,smbcommerce_ai_notice_text:t.smbcommerce_ai_notice_text,smbcommerce_hide_on_checkout:t.smbcommerce_hide_on_checkout,smbcommerce_voice_input_enabled:t.smbcommerce_voice_input_enabled,smbcommerce_reply_language:t.smbcommerce_reply_language,smbcommerce_coupon_allowlist:t.smbcommerce_coupon_allowlist,smbcommerce_tracking_code_meta:t.smbcommerce_tracking_code_meta,smbcommerce_tracking_carrier_meta:t.smbcommerce_tracking_carrier_meta,smbcommerce_tracking_carrier:t.smbcommerce_tracking_carrier,smbcommerce_tracking_url_template:t.smbcommerce_tracking_url_template,smbcommerce_abilities_enabled:t.smbcommerce_abilities_enabled,smbcommerce_gemini_model:t.smbcommerce_gemini_model,smbcommerce_anthropic_model:t.smbcommerce_anthropic_model,smbcommerce_openai_model:t.smbcommerce_openai_model,smbcommerce_refund_policy:t.smbcommerce_refund_policy,smbcommerce_warranty_policy:t.smbcommerce_warranty_policy,smbcommerce_delivery_policy:t.smbcommerce_delivery_policy,smbcommerce_privacy_policy:t.smbcommerce_privacy_policy,smbcommerce_enable_link_reading:t.smbcommerce_enable_link_reading,smbcommerce_budget_daily_usd:t.smbcommerce_budget_daily_usd,smbcommerce_budget_monthly_usd:t.smbcommerce_budget_monthly_usd,smbcommerce_enable_provider_fallback:t.smbcommerce_enable_provider_fallback,smbcommerce_block_ungrounded_prices:t.smbcommerce_block_ungrounded_prices,smbcommerce_enable_response_cache:t.smbcommerce_enable_response_cache,smbcommerce_cache_ttl_hours:t.smbcommerce_cache_ttl_hours,smbcommerce_webhook_url:t.smbcommerce_webhook_url};t.smbcommerce_webhook_secret&&(a.smbcommerce_webhook_secret=t.smbcommerce_webhook_secret),t.smbcommerce_openai_key&&(a.smbcommerce_openai_key=t.smbcommerce_openai_key),t.smbcommerce_anthropic_key&&(a.smbcommerce_anthropic_key=t.smbcommerce_anthropic_key),t.smbcommerce_gemini_key&&(a.smbcommerce_gemini_key=t.smbcommerce_gemini_key),t.smbcommerce_telegram_bot_token&&(a.smbcommerce_telegram_bot_token=t.smbcommerce_telegram_bot_token);const E=await fetch(`${H}/admin/settings`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify(a)});if(!E.ok){const Y=await E.json().catch(()=>null);throw new Error((Y==null?void 0:Y.message)||"Save failed")}g("Settings saved successfully!"),n(Y=>({...Y,smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:""})),me()}catch(a){const E=a instanceof Error&&a.message!=="Save failed"?` ${a.message}`:"";g(`Error saving settings.${E}`),console.error(a)}finally{k(!1),setTimeout(()=>g(""),8e3)}},Te=()=>{r.ask("Restore the default playbook? Your current text in this field will be replaced.","Restore default",Ie)},Ie=async()=>{try{const a=await(await fetch(`${H}/admin/playbook/default`,{headers:w.getHeaders()})).json();n(E=>({...E,smbcommerce_sales_playbook:a.playbook||""})),g("Default playbook loaded. Click Save to apply."),setTimeout(()=>g(""),4e3)}catch{g("Could not load the default playbook.")}},Le=async()=>{I(!0),$(null);try{const a=await(await fetch(`${H}/admin/products/sync`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})})).json();$(a),de()}catch(s){$({error:s.message})}finally{I(!1)}},de=async()=>{try{const s=await fetch(`${H}/admin/products/status`,{headers:w.getHeaders()});i(await s.json())}catch{}},Re=async()=>{N(!0),T(null);try{const a=await(await fetch(`${H}/admin/ai-test`,{headers:w.getHeaders()})).json();T(a)}catch(s){T({error:s.message})}finally{N(!1)}},Ee=()=>{const s=window.wp;if(!s||!s.media){g("The WordPress media library is not available on this page. Paste the logo URL instead."),setTimeout(()=>g(""),8e3);return}const a=s.media({title:"Select Logo",button:{text:"Use this logo"},multiple:!1});a.on("select",()=>{const E=a.state().get("selection").first().toJSON();n({...t,smbcommerce_logo_url:E.url})}),a.open()};return e("div",{className:"smb-commerce-settings-container",children:[h&&e("div",{className:"smb-commerce-notice",role:"status",children:e("p",{children:h})}),e(le,{request:r.request,onCancel:r.cancel}),e("div",{className:"smb-commerce-settings-layout",children:[e("div",{className:"smb-commerce-settings-form-col",children:e("form",{onSubmit:Ae,className:"smb-commerce-form",children:[e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-art"})," Widget appearance"]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Brand name"}),e("input",{type:"text",value:t.smbcommerce_brand_name,onChange:s=>n({...t,smbcommerce_brand_name:s.target.value})}),e("p",{className:"description",children:"The name shown in the chat window header."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Font"}),e("select",{value:t.smbcommerce_font_family,onChange:s=>n({...t,smbcommerce_font_family:s.target.value}),children:[e("option",{value:"system",children:"System font (no external requests)"}),e("option",{value:"Inter",children:"Inter (Google Fonts)"}),e("option",{value:"Roboto",children:"Roboto (Google Fonts)"}),e("option",{value:"Open Sans",children:"Open Sans (Google Fonts)"}),e("option",{value:"Montserrat",children:"Montserrat (Google Fonts)"}),e("option",{value:"Poppins",children:"Poppins (Google Fonts)"})]}),e("p",{className:"description",children:"Google Fonts are loaded from fonts.googleapis.com in your visitors' browsers."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Logo URL"}),e("div",{style:{display:"flex",gap:"8px"},children:[e("input",{type:"text",value:t.smbcommerce_logo_url,onChange:s=>n({...t,smbcommerce_logo_url:s.target.value}),placeholder:"https://...",style:{flex:1}}),e("button",{type:"button",className:"button",onClick:Ee,children:"Upload image"})]}),e("p",{className:"description",children:"Shown on the left of the widget header."})]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Primary colour"}),e("div",{className:"color-picker-wrapper",children:[e("input",{type:"color",value:t.smbcommerce_primary_color,onChange:s=>n({...t,smbcommerce_primary_color:s.target.value})}),e("span",{className:"color-hex",children:t.smbcommerce_primary_color})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Footer tagline"}),e("input",{type:"text",placeholder:"AI sales assistant, available 24/7",value:t.smbcommerce_widget_slogan,onChange:s=>n({...t,smbcommerce_widget_slogan:s.target.value})})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Other ways to reach you"}),e("p",{className:"description",children:"Shown as small icons under the chat box. Leave a field empty to hide that channel."}),e("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"10px",marginTop:"5px"},children:[e("input",{type:"text","aria-label":"WhatsApp number or link",placeholder:"WhatsApp number (e.g. +1 555 0100)",value:t.smbcommerce_whatsapp||"",onChange:s=>n({...t,smbcommerce_whatsapp:s.target.value})}),e("input",{type:"text","aria-label":"Messenger link",placeholder:"Messenger link (e.g. https://m.me/...)",value:t.smbcommerce_messenger_link||"",onChange:s=>n({...t,smbcommerce_messenger_link:s.target.value})}),e("input",{type:"text","aria-label":"Telegram username or link",placeholder:"Telegram (e.g. @yourshop)",value:t.smbcommerce_telegram_link||"",onChange:s=>n({...t,smbcommerce_telegram_link:s.target.value})}),e("input",{type:"text","aria-label":"Zalo link",placeholder:"Zalo link (e.g. https://zalo.me/...)",value:t.smbcommerce_zalo_link||"",onChange:s=>n({...t,smbcommerce_zalo_link:s.target.value})}),e("input",{type:"text","aria-label":"Phone number",placeholder:"Phone (e.g. +1 555 0100)",value:t.smbcommerce_hotline||"",onChange:s=>n({...t,smbcommerce_hotline:s.target.value})})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_ai_notice_enabled==="1",onChange:s=>n({...t,smbcommerce_ai_notice_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Tell visitors they are chatting with AI"}),e("p",{className:"description",style:{margin:0},children:"Shows a short line at the top of the chat. Recommended, and expected by AI transparency rules in many countries."})]})]}),t.smbcommerce_ai_notice_enabled==="1"&&e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-ai-notice",children:"AI notice text"}),e("input",{id:"smb-ai-notice",type:"text",maxLength:200,placeholder:"You are chatting with an AI assistant. A team member can join when needed.",value:t.smbcommerce_ai_notice_text,onChange:s=>n({...t,smbcommerce_ai_notice_text:s.target.value})}),e("p",{className:"description",children:"Leave empty to use the default text, translated into the site language."})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_hide_on_checkout==="1",onChange:s=>n({...t,smbcommerce_hide_on_checkout:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Hide the chat on the checkout page"}),e("p",{className:"description",style:{margin:0},children:"Off by default. The chat button already moves up on cart and checkout pages so it does not cover the order button."})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_voice_input_enabled==="1",onChange:s=>n({...t,smbcommerce_voice_input_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let visitors speak their message"}),e("p",{className:"description",style:{margin:0},children:"Adds a microphone button. The visitor's browser turns speech into text in the message box, and the visitor checks it before sending. The plugin never receives or stores audio, but depending on the browser the audio may be sent to the browser maker's speech service (for example Google for Chrome, Apple for Safari). Shown only on HTTPS pages in browsers that support it (not Firefox). Off by default."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Suggested question chips"}),e("textarea",{style:{width:"100%",height:"80px",padding:"12px"},placeholder:`One per line. For example:
    22Help me choose a plan
    33Do you ship internationally?
    4 What is your return policy?`,value:t.smbcommerce_suggestion_chips,onChange:n=>s({...t,smbcommerce_suggestion_chips:n.target.value})}),e("p",{className:"description",children:"One suggestion per line. Visitors can tap one to send that question instantly."})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-network"})," AI configuration"]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Assistant mode"}),e("select",{value:t.smbcommerce_runtime_mode,onChange:n=>s({...t,smbcommerce_runtime_mode:n.target.value}),children:[e("option",{value:"smart_hybrid",children:"Smart hybrid — answer locally where possible, use AI when needed (recommended)"}),e("option",{value:"local_only",children:"Local only — never send conversations to an AI provider"}),e("option",{value:"ai_enhanced",children:"AI enhanced — let the AI lead every reply"})]}),e("p",{className:"description",children:"Local only answers greetings, policy questions, cart and order lookups from your own store data and costs no tokens. It cannot answer anything else — those customers are asked to contact you directly. This setting covers AI providers only; Telegram notifications, webhooks and licensing still reach the internet."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI provider"}),e("select",{value:t.smbcommerce_ai_provider,onChange:n=>s({...t,smbcommerce_ai_provider:n.target.value}),children:[e("option",{value:"openai",children:"OpenAI (GPT-4o-mini)"}),e("option",{value:"anthropic",children:"Anthropic (Claude 3.5)"}),e("option",{value:"gemini",children:"Google Gemini"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI API Key"}),e("input",{type:"password",placeholder:a.openai?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_openai_key,onChange:n=>s({...t,smbcommerce_openai_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Anthropic API Key"}),e("input",{type:"password",placeholder:a.anthropic?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_anthropic_key,onChange:n=>s({...t,smbcommerce_anthropic_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini API Key"}),e("input",{type:"password",placeholder:a.gemini?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_gemini_key,onChange:n=>s({...t,smbcommerce_gemini_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini model"}),e("input",{type:"text",placeholder:"Leave empty for gemini-flash-latest",value:t.smbcommerce_gemini_model||"",onChange:n=>s({...t,smbcommerce_gemini_model:n.target.value})}),e("p",{className:"description",children:["Leave empty to always use Google's current Flash model. Enter a specific model (for example ",e("code",{children:"gemini-3.5-flash"}),") only if you need it."]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Claude Model"}),e("input",{type:"text",placeholder:"Leave empty for claude-sonnet-5-5",value:t.smbcommerce_anthropic_model,onChange:n=>s({...t,smbcommerce_anthropic_model:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI Model"}),e("input",{type:"text",placeholder:"Leave empty for gpt-4o-mini",value:t.smbcommerce_openai_model,onChange:n=>s({...t,smbcommerce_openai_model:n.target.value})})]}),W&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Models in use"}),["anthropic","openai","gemini"].filter(n=>{var c;return(c=W[n])==null?void 0:c.retired}).map(n=>e("div",{className:"notice notice-warning inline",children:e("p",{children:["The saved ",n==="anthropic"?"Claude":n==="openai"?"OpenAI":"Gemini"," model ",e("code",{children:W[n].configured})," is no longer available from the provider. The assistant is using ",e("code",{children:W[n].model})," instead. Enter a current model above to choose for yourself."]})},n)),((ue=W[t.smbcommerce_ai_provider])==null?void 0:ue.source)==="default"&&e("div",{className:"notice notice-info inline",children:e("p",{children:["No model is chosen for your main provider, so the assistant uses the default ",e("code",{children:W[t.smbcommerce_ai_provider].model}),". Choosing a model yourself keeps your cost predictable when defaults change."]})}),e("table",{className:"widefat striped",style:{marginTop:8},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Provider"}),e("th",{children:"Model used"}),e("th",{children:"Estimated cost per conversation"})]})}),e("tbody",{children:["anthropic","openai","gemini"].map(n=>{const c=W[n];return c?e("tr",{children:[e("td",{children:[n==="anthropic"?"Claude":n==="openai"?"OpenAI":"Gemini",t.smbcommerce_ai_provider===n?" (main)":""]}),e("td",{children:[e("code",{children:c.model})," ",c.source==="default"?"(default)":c.source==="replacement"?"(replacement)":""]}),e("td",{children:c.estimate_per_conversation===null?"No price on file":`about $${Number(c.estimate_per_conversation).toFixed(4)}`})]},n):null})})]}),e("p",{className:"description",children:"Estimate for a typical conversation: 2 replies with about 7,000 input and 80 output tokens each, at the provider's list price. Your real cost is recorded per request on the AI Value & Performance tab."})]})]}),A&&A.managed&&e("div",{className:"smb-commerce-card",style:{borderLeft:`4px solid ${A.is_paid?"#1b7a51":"#9a5c06"}`},children:[e("h3",{children:[e("span",{className:"dashicons dashicons-awards"})," Licence"]}),e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:200},children:"Current plan"}),e("td",{children:e("strong",{children:A.label})})]}),e("tr",{children:[e("td",{children:"Valid until"}),e("td",{children:A.is_lifetime?"Lifetime, never expires":A.expires_at||"—"})]})]})}),!A.is_paid&&e("p",{className:"description",style:{marginBottom:10},children:"These features need a paid plan: business analytics, AI budget caps, webhooks, lead scoring, provider failover, response cache, ungrounded-figure blocking and product variation tools. Chat, product sync, the sales playbook and lead capture keep working as normal."}),A.tier==="starter"&&e("p",{className:"description",style:{marginBottom:10},children:"The Pro plan adds provider failover, AI budget caps, coupon validation, lead scoring, advanced analytics, price verification and outgoing webhooks."}),A.upgrade_url&&A.tier!=="pro"&&e("a",{className:"button button-primary",href:A.upgrade_url,children:"View pricing"}),A.account_url&&e("a",{className:"button",style:{marginLeft:8},href:A.account_url,children:"Manage licence"})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-chart-bar"})," Business performance"]}),e("p",{className:"description",style:{marginTop:0},children:["Last ",H," days."," ",[7,30,90].map(n=>e("button",{type:"button",className:"button button-small",style:{marginRight:4,fontWeight:H===n?700:400},onClick:()=>U(n),children:[n," days"]},n))]}),h&&h.locked?e("p",{className:"description",children:"A paid plan is required to view analytics. Data is still being recorded — upgrade and your full history appears immediately."}):h&&h.summary?e("div",{children:[e("table",{className:"widefat striped",style:{marginBottom:14},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:260},children:"Conversations"}),e("td",{children:e("strong",{children:h.summary.conversations})})]}),e("tr",{children:[e("td",{children:"Leads captured"}),e("td",{children:[e("strong",{children:h.summary.leads})," (",h.summary.lead_rate,"% of conversations) · ",h.summary.hot_leads," hot"]})]}),e("tr",{children:[e("td",{children:"Not handed off to staff"}),e("td",{children:[e("strong",{children:[h.summary.resolution_rate,"%"]})," · handed off ",h.summary.handoff_rate,"%"]})]}),e("tr",{children:[e("td",{children:"AI cost (estimated)"}),e("td",{children:e("strong",{children:J(h.summary.cost_usd)})})]}),e("tr",{children:[e("td",{children:"Cost per conversation"}),e("td",{children:J(h.summary.cost_per_conversation)})]}),e("tr",{children:[e("td",{children:"Cost per lead"}),e("td",{children:h.summary.leads>0?J(h.summary.cost_per_lead):"—"})]}),e("tr",{children:[e("td",{children:"AI calls · average latency"}),e("td",{children:[h.summary.ai_calls," calls · ",h.summary.avg_latency_ms,"ms · ",h.summary.error_rate,"% errors"]})]}),e("tr",{children:[e("td",{children:"Tokens used"}),e("td",{children:(h.summary.prompt_tokens+h.summary.completion_tokens).toLocaleString()})]})]})}),h.budget&&(h.budget.daily_limit>0||h.budget.monthly_limit>0)&&e("p",{className:"description",children:[h.budget.daily_limit>0&&e("span",{children:["Today: ",J(h.budget.daily_spent)," / ",J(h.budget.daily_limit)," (",h.budget.daily_percent,"%). "]}),h.budget.monthly_limit>0&&e("span",{children:["This month: ",J(h.budget.monthly_spent)," / ",J(h.budget.monthly_limit)," (",h.budget.monthly_percent,"%)."]})]}),h.cache&&h.cache.hits>0&&e("p",{className:"description",children:["Cache: ",h.cache.entries," entries stored, ",h.cache.saved_calls," model calls saved."]}),h.handoffs&&h.handoffs.length>0&&e("div",{style:{marginTop:14},children:[e("strong",{style:{fontSize:13},children:"Recently handed off to a human"}),e("p",{className:"description",style:{marginTop:2},children:"This is where the bot ran out of knowledge — worth reading to fill the gaps."}),e("table",{className:"widefat striped",children:e("tbody",{children:h.handoffs.map(n=>e("tr",{children:[e("td",{style:{width:150},children:n.updated_at}),e("td",{style:{width:70},children:n.score}),e("td",{children:n.intent||n.summary||"—"})]},n.id))})})]})]}):e("p",{className:"description",children:"No data yet. Figures appear after the first conversation."})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-shield"})," Cost & reliability"]}),!(v("budget")&&v("provider_failover")&&v("output_guard"))&&e("p",{className:"description",children:["AI budget caps, provider failover and price verification need the Pro plan.",!v("response_cache")&&" The response cache needs the Starter plan."]}),v("budget")&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI budget cap (USD)"}),e("div",{style:{display:"flex",gap:10,flexWrap:"wrap"},children:[e("input",{type:"number",min:"0",step:"0.5",style:{maxWidth:150},placeholder:"Per day",value:t.smbcommerce_budget_daily_usd,onChange:n=>s({...t,smbcommerce_budget_daily_usd:n.target.value})}),e("input",{type:"number",min:"0",step:"1",style:{maxWidth:150},placeholder:"Per month",value:t.smbcommerce_budget_monthly_usd,onChange:n=>s({...t,smbcommerce_budget_monthly_usd:n.target.value})})]}),e("p",{className:"description",children:["Set to 0 for no limit. When the cap is reached the bot stops replying and hands the conversation to your team; the visitor never sees anything about billing. Cost is an",e("strong",{children:" estimate"})," based on published rates, not your actual invoice."]})]}),v("provider_failover")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_provider_fallback==="1",onChange:n=>s({...t,smbcommerce_enable_provider_fallback:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Automatic provider failover"}),e("p",{className:"description",style:{margin:0},children:"If the primary provider fails or is rate limited, try another provider that already has an API key instead of showing the visitor an error."})]})]}),v("output_guard")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_block_ungrounded_prices==="1",onChange:n=>s({...t,smbcommerce_block_ungrounded_prices:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Block replies containing ungrounded figures"}),e("p",{className:"description",style:{margin:0},children:"Every price figure in a reply is checked against the real data given to the AI. When off, mismatches are still logged but not blocked — turn this on once the log shows few false positives."})]})]}),v("response_cache")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_response_cache==="1",onChange:n=>s({...t,smbcommerce_enable_response_cache:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Cache repeated questions"}),e("p",{className:"description",style:{margin:0},children:["Identical questions reuse the previous answer. Questions about price, stock, orders and coupons are never cached.",e("input",{type:"number",min:"1",style:{width:80,marginLeft:8},value:t.smbcommerce_cache_ttl_hours,onChange:n=>s({...t,smbcommerce_cache_ttl_hours:n.target.value})})," hours",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:Me,children:"Clear cache"})]})]})]})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," Outgoing webhooks"]}),v("webhooks")?e(z,{children:[e("p",{className:"description",style:{marginTop:0},children:["Send events to n8n, Make, Zapier or your CRM. Events:"," ",e("code",{children:"lead.created"})," ",e("code",{children:"lead.qualified"})," ",e("code",{children:"human.requested"})," ",e("code",{children:"budget.exceeded"})," ",e("code",{children:"ai.failed"})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Webhook URL"}),e("input",{type:"url",placeholder:"https://n8n.example.com/webhook/...",value:t.smbcommerce_webhook_url,onChange:n=>s({...t,smbcommerce_webhook_url:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Signing secret"}),e("input",{type:"password",placeholder:t.smbcommerce_webhook_secret_exists?"•••••••••• (saved)":"Any secret string",value:t.smbcommerce_webhook_secret,onChange:n=>s({...t,smbcommerce_webhook_secret:n.target.value})}),e("p",{className:"description",children:["With a secret set, every request carries the header ",e("code",{children:"X-SMB-Commerce-Signature: sha256=..."}),", computed with HMAC-SHA256 over the body. The receiver verifies it to confirm the request came from this site."]})]}),e("button",{type:"button",className:"button",onClick:ee,children:"Send test"}),O&&e("p",{className:"description",style:{marginTop:8},children:O.pending?"Sending...":O.success?`Sent. The receiver returned HTTP ${O.status}${O.signed?", signed":", unsigned"}.`:`Error: ${O.message||"unknown error"}`})]}):e("p",{className:"description",style:{marginTop:0},children:"Outgoing webhooks are available in the Pro plan."})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-megaphone"})," Sales playbook"]}),e("p",{className:"description",style:{marginTop:0},children:["This is what decides whether the bot actually sells. Available variables:",e("code",{children:"{brand}"})," ",e("code",{children:"{hotline}"})," ",e("code",{children:"{zalo}"})," ",e("code",{children:"{messenger}"}),"."]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Main playbook"}),e("textarea",{rows:16,style:{fontFamily:"monospace",fontSize:"12px"},value:t.smbcommerce_sales_playbook,onChange:n=>s({...t,smbcommerce_sales_playbook:n.target.value})}),e("p",{className:"description",children:["Role, tone, sales flow, objection handling and how to ask for contact details.",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:ze,children:"Restore default playbook"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Business information"}),e("textarea",{rows:6,style:{fontFamily:"monospace",fontSize:"12px"},placeholder:"Business hours, payment, delivery, returns...",value:t.smbcommerce_business_info,onChange:n=>s({...t,smbcommerce_business_info:n.target.value})}),k!==null&&t.smbcommerce_business_info===k&&e("div",{className:"notice notice-warning inline",children:e("p",{children:"This is the sample text added by an earlier version, not your store's details. The assistant does not use it. Replace it with your real opening hours, payment, delivery and return terms, then click Save."})}),e("p",{className:"description",children:"The bot may answer policy questions only from this text. Leave it empty and the bot hands off to your team instead of guessing."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:["Detailed policies (",e("code",{children:"get_policy"}),")"]}),e("p",{className:"description",style:{marginTop:0},children:"Any field left empty falls back to the Business information above for that policy type."}),[["smbcommerce_refund_policy","Refunds"],["smbcommerce_warranty_policy","Warranty"],["smbcommerce_delivery_policy","Delivery"],["smbcommerce_privacy_policy","Privacy"]].map(([n,c])=>e("div",{style:{marginBottom:10},children:[e("label",{style:{fontWeight:500},children:c}),e("textarea",{rows:3,value:t[n],onChange:L=>s({...t,[n]:L.target.value})})]},n))]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_adaptive_playbook==="1",onChange:n=>s({...t,smbcommerce_enable_adaptive_playbook:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Situational guidance"}),e("p",{className:"description",style:{margin:0},children:"Detects whether the visitor is asking about price, pushing back on cost, hesitating, comparing or ready to buy, then adds guidance for that specific turn. Also reminds the bot to ask for contact details after a few exchanges."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-products"})," Product data"]}),e("p",{className:"description",style:{marginTop:0},children:"The bot can only recommend products that have been synced into its cache. Without a sync it has nothing to sell."}),C&&e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:220},children:"WooCommerce"}),e("td",{children:C.woocommerce_active?"Active":"NOT ACTIVE"})]}),e("tr",{children:[e("td",{children:"Products in cache"}),e("td",{children:e("strong",{children:C.total_cached})})]}),e("tr",{children:[e("td",{children:"Last sync"}),e("td",{children:C.last_sync||"Never"})]}),e("tr",{children:[e("td",{children:"Next scheduled run"}),e("td",{children:C.next_scheduled_sync||"Not scheduled"})]})]})}),e("button",{type:"button",className:"button",onClick:Ue,disabled:$,children:$?"Syncing...":"Sync products now"}),I&&e("p",{className:"description",style:{marginTop:8},children:I.success?`Synced ${I.synced} products. Total in cache: ${I.total_cached}.`:`Error: ${I.message||I.error||"unknown error"}`})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-tools"})," Advanced features"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_link_reading==="1",onChange:n=>s({...t,smbcommerce_enable_link_reading:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Let the bot read links visitors send"}),e("p",{className:"description",style:{margin:0},children:["When enabled, every URL a visitor pastes into the chat is sent to the external service ",e("code",{children:"r.jina.ai"})," to fetch its content. Leave this off if visitors might paste private or internal links. Off by default."]})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Maximum upload size (MB)"}),e("input",{type:"number",value:t.smbcommerce_max_upload_size,onChange:n=>s({...t,smbcommerce_max_upload_size:n.target.value}),min:"1",max:"50"}),e("p",{className:"description",children:"5MB is recommended to avoid filling up server storage."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"File retention (days)"}),e("input",{type:"number",value:t.smbcommerce_media_retention_days,onChange:n=>s({...t,smbcommerce_media_retention_days:n.target.value}),min:"1",max:"365"}),e("p",{className:"description",children:'Files (images, documents) in CLOSED conversations older than this are deleted automatically to keep the server clean. Their links are replaced with an "Expired" notice.'})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"System guardrails prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"e.g. You are a customer support AI. Never disclose sensitive information...",value:t.smbcommerce_system_guardrails_prompt,onChange:n=>s({...t,smbcommerce_system_guardrails_prompt:n.target.value})}),e("p",{className:"description",children:"Highest-priority guardrail, always pinned at the top of the context to resist jailbreaks and hallucination."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Sales & CSKH Flow Prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"Enter your sales playbook here...",value:t.smbcommerce_meta_prompt_template,onChange:n=>s({...t,smbcommerce_meta_prompt_template:n.target.value})})]}),v("telegram")?e(z,{children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram bot token"}),e("input",{type:"password",placeholder:a.telegram?"•••••••••••• (saved)":"Enter the bot token from BotFather...",value:t.smbcommerce_telegram_bot_token,onChange:n=>s({...t,smbcommerce_telegram_bot_token:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram Chat ID"}),e("input",{type:"text",placeholder:"e.g. -1001234567890",value:t.smbcommerce_telegram_chat_id,onChange:n=>s({...t,smbcommerce_telegram_chat_id:n.target.value})}),e("p",{className:"description",children:"Without a Chat ID new-lead notifications cannot be delivered, even with a Bot Token set."})]})]}):e("p",{className:"description",id:"smb-commerce-telegram-plan-note",children:"Telegram notifications are available in the Starter plan."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Handoff Telegram Webhook URL"}),e("input",{type:"text",placeholder:"https://...",value:t.smbcommerce_handoff_webhook,onChange:n=>s({...t,smbcommerce_handoff_webhook:n.target.value})}),e("p",{className:"description",children:"URL that receives an urgent notification whenever a handoff is triggered."})]})]}),e("div",{className:"smb-commerce-actions",children:[e("button",{type:"submit",className:"button button-primary button-large",disabled:d,children:d?"Saving...":"Save all settings"}),e("button",{type:"button",className:"button button-secondary",onClick:Ve,disabled:y,style:{marginLeft:"10px"},children:y?"Testing...":"Test AI connection"})]}),_&&e("div",{style:{marginTop:"16px",padding:"14px 18px",borderRadius:"8px",background:_.error?"#fff0f0":(pe=_.test_result)!=null&&pe.startsWith("OK")?"#f0fff4":"#fffbe6",border:`1px solid ${_.error?"#ffcdd2":(_e=_.test_result)!=null&&_e.startsWith("OK")?"#a7f3d0":"#fde68a"}`,fontSize:"13px",lineHeight:"1.7"},children:_.error?e("div",{children:[e("strong",{children:"Error:"})," ",_.error]}):e(z,{children:[e("div",{children:[e("strong",{children:"Selected provider:"})," ",_.configured_provider]}),e("div",{children:[e("strong",{children:"Class:"})," ",_.provider_class]}),e("div",{children:[e("strong",{children:"Gemini key:"})," ",_.has_gemini_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"OpenAI key:"})," ",_.has_openai_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"Claude key:"})," ",_.has_claude_key?"Saved":"Not set"]}),e("div",{style:{marginTop:"8px",padding:"8px",background:"#fff",borderRadius:"6px",border:"1px solid #e5e7eb"},children:[e("strong",{children:"Test result:"}),e("br",{}),e("code",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:_.test_result||"No provider"})]})]})})]})}),e("div",{className:"smb-commerce-settings-preview-col",children:e("div",{className:"preview-sticky",children:[e("h3",{children:"Live preview"}),e("p",{className:"description",children:"How the widget will look on your site."}),e("div",{className:"widget-preview-box",style:t.smbcommerce_font_family&&t.smbcommerce_font_family!=="system"?{fontFamily:`'${t.smbcommerce_font_family}', sans-serif`}:void 0,children:[e("div",{className:"widget-preview-header",style:{backgroundColor:t.smbcommerce_primary_color},children:[e("div",{className:"widget-preview-header-brand",children:[t.smbcommerce_logo_url?e("img",{src:t.smbcommerce_logo_url,alt:"Logo",className:"widget-preview-logo"}):e("svg",{viewBox:"0 0 24 24",width:"24",height:"24",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),e("span",{children:t.smbcommerce_brand_name||"Brand name"})]}),e("div",{className:"widget-preview-close",children:"×"})]}),e("div",{className:"widget-preview-body",children:[e("div",{className:"preview-message bot",children:"Hello! How can I help you today?"}),e("div",{className:"preview-message user",style:{backgroundColor:t.smbcommerce_primary_color,color:"#fff"},children:"I'd like to ask about your services."})]}),e("div",{className:"widget-preview-footer",children:[e("button",{style:{background:"transparent",border:"none",color:"#666",padding:"0 8px"},children:e("svg",{viewBox:"0 0 24 24",width:"20",height:"20",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"})})}),e("input",{type:"text",placeholder:"Type a message...",disabled:!0}),e("button",{style:{backgroundColor:t.smbcommerce_primary_color},children:e("svg",{viewBox:"0 0 24 24",width:"16",height:"16",stroke:"currentColor",strokeWidth:"2",fill:"none",children:[e("line",{x1:"22",y1:"2",x2:"11",y2:"13"}),e("polygon",{points:"22 2 15 22 11 13 2 9 22 2"})]})})]})]})]})})]})]})}var $e;const _t=(($e=window.SMBCommerceAdmin)==null?void 0:$e.i18n)||{};function l(r,...t){let s=0;return(_t[r]||r).replace(/%(\d+)\$s/g,(a,o)=>String(t[Number(o)-1]??"")).replace(/%s/g,()=>String(t[s++]??"")).replace(/%%/g,"%")}const F=r=>{const t=Number(r)||0,s=t<0?"−":"",a=Math.abs(t);return`${s}$${a.toFixed(a>0&&a<1?4:2)}`},bt=(r,t)=>{try{return new Intl.NumberFormat(void 0,{style:"currency",currency:t}).format(r)}catch{return`${r.toFixed(2)} ${t}`}},gt=r=>r?new Date(r.replace(" ","T")+"Z").toLocaleDateString():"",We={24:"hours24",72:"hours72",168:"days7"},ae={processing:"statusProcessing",completed:"statusCompleted","on-hold":"statusOnHold"},oe={ai_conversation:"unitAiConversation",resolved_conversation:"unitResolved",month:"unitMonth"},ft=[["ai","sourceAi"],["rule","sourceRule"],["customer","sourceCustomer"],["agent","sourceAgent"],["budget","sourceBudget"]];function yt(){const{notices:r,push:t,dismiss:s}=ce(),[a,o]=p(30),[d,i]=p(null),[f,b]=p(!0),[_,E]=p(!1),[y,w]=p(null),[$,B]=p(!1),I=async u=>{b(!0),E(!1);try{const h=await N.get(`/admin/value-metrics?days=${u}`);i(h),w(S=>S??h.settings)}catch{E(!0)}finally{b(!1)}};j(()=>{I(a)},[a]);const D=async u=>{if(u.preventDefault(),!!y){B(!0);try{const h=await N.post("/admin/value-settings",y);w(h),t("success",l("saved")),I(a)}catch{t("error",l("saveError"))}finally{B(!1)}}},C=(u,h)=>w(S=>S&&{...S,reference:{...S.reference,[u]:h}});return e("section",{className:"smb-value","aria-labelledby":"smb-value-title",children:[e(ie,{notices:r,onDismiss:s}),e("div",{className:"smb-value-header",children:[e("div",{children:[e("h2",{id:"smb-value-title",children:l("tab")}),e("p",{className:"smb-value-intro",children:l("intro")}),d&&e("p",{className:"smb-value-meta",children:d.measurement_started_at?l("measuredSince",gt(d.measurement_started_at)):l("notMeasured")})]}),e("label",{className:"smb-value-period",children:[e("span",{children:l("period")}),e("select",{value:a,onChange:u=>o(Number(u.target.value)),children:[7,30,90].map(u=>e("option",{value:u,children:l("lastDays",u)},u))})]})]}),f&&e("p",{role:"status",className:"smb-value-status",children:l("loading")}),_&&e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:l("loadError")})}),d&&!_&&e("div",{className:"smb-value-grid","aria-busy":f,children:[e(vt,{cost:d.cost}),e(wt,{orders:d.orders,allowed:d.revenue_allowed}),e(kt,{outcomes:d.outcomes,sources:d.handoffs_by_source})]}),y&&e("details",{className:"smb-value-settings",children:[e("summary",{children:l("settingsTitle")}),e("form",{onSubmit:D,children:[e("fieldset",{children:[e("legend",{children:l("costTitle")}),e("p",{className:"description",children:l("refHelp")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[l("refLabel"),e("input",{type:"text",maxLength:80,value:y.reference.label,onChange:u=>C("label",u.target.value)})]}),e("label",{children:[l("refUnit"),e("select",{value:y.reference.unit,onChange:u=>C("unit",u.target.value),children:Object.keys(oe).map(u=>e("option",{value:u,children:l(oe[u])},u))})]}),e("label",{children:[l("refPrice"),e("input",{type:"number",min:"0",step:"0.0001",value:y.reference.price,onChange:u=>C("price",Number(u.target.value))})]}),e("label",{children:[l("refSource"),e("input",{type:"text",maxLength:200,value:y.reference.source,onChange:u=>C("source",u.target.value)})]}),e("label",{children:[l("refDate"),e("input",{type:"date",value:y.reference.date,onChange:u=>C("date",u.target.value)})]}),e("label",{children:[l("pluginMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:y.plugin_cost_monthly,onChange:u=>w({...y,plugin_cost_monthly:Number(u.target.value)})})]}),e("label",{children:[l("otherMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:y.other_cost_monthly,onChange:u=>w({...y,other_cost_monthly:Number(u.target.value)})})]})]})]}),e("fieldset",{children:[e("legend",{children:l("ordersTitle")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[l("window"),e("select",{value:y.window_hours,onChange:u=>w({...y,window_hours:Number(u.target.value)}),"aria-describedby":"smb-window-help",children:[24,72,168].map(u=>e("option",{value:u,children:l(We[u])},u))}),e("span",{id:"smb-window-help",className:"description",children:l("windowHelp")})]}),e("div",{role:"group","aria-labelledby":"smb-statuses-label",children:[e("span",{id:"smb-statuses-label",className:"smb-value-group-label",children:l("statuses")}),Object.keys(ae).map(u=>e("label",{className:"smb-value-check",children:[e("input",{type:"checkbox",checked:y.statuses.includes(u),onChange:h=>w({...y,statuses:h.target.checked?[...y.statuses,u]:y.statuses.filter(S=>S!==u)})}),l(ae[u])]},u))]})]})]}),e("button",{type:"submit",className:"button button-primary",disabled:$,children:l($?"saving":"save")})]})]})]})}function vt({cost:r}){const t=r.reference;return e("article",{className:"smb-value-card","aria-labelledby":"smb-cost-title",children:[e("h3",{id:"smb-cost-title",children:l("costTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.ai_conversations)}),e("dl",{className:"smb-value-rows",children:[e("div",{children:[e("dt",{children:l("aiCost")}),e("dd",{children:[e("strong",{children:F(r.ai_cost_total)}),e("span",{className:"smb-value-sub",children:l("aiCostSplit",F(r.ai_cost_usage),F(r.ai_cost_estimated))}),r.unpriced_requests>0&&e("span",{className:"smb-value-sub",children:l("unpriced",r.unpriced_requests)})]})]}),e("div",{children:[e("dt",{children:l("pluginCost")}),e("dd",{children:F(r.plugin_cost)})]}),e("div",{children:[e("dt",{children:l("otherCost")}),e("dd",{children:F(r.other_cost)})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:l("totalCost")}),e("dd",{children:e("strong",{children:F(r.total_cost)})})]}),t&&e(z,{children:[e("div",{children:[e("dt",{children:l("referenceCost",t.label)}),e("dd",{children:[F(t.cost),e("span",{className:"smb-value-sub",children:[l("referenceVolume",t.volume,F(t.price))," ",l(oe[t.unit]||"unitAiConversation")]}),t.source&&e("span",{className:"smb-value-sub",children:[l("referenceSource",t.source),t.date?` · ${t.date}`:""]})]})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:l("difference")}),e("dd",{children:e("strong",{children:F(t.difference)})})]})]})]}),e("p",{className:"smb-value-note",children:l(t?"differenceNote":"noReference")}),e("p",{className:"smb-value-note",children:l("costScope")})]})}function wt({orders:r,allowed:t}){const s=r.revenue_by_currency||{},a=(r.statuses||[]).map(o=>l(ae[o]||o)).join(", ");return e("article",{className:"smb-value-card","aria-labelledby":"smb-orders-title",children:[e("h3",{id:"smb-orders-title",children:l("ordersTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.assisted_orders)}),e("p",{className:"smb-value-big",children:[e("strong",{children:r.assisted_orders})," ",e("span",{children:l("orders")})]}),e("p",{className:"smb-value-caption",children:l("assistedCaption")}),e("h4",{children:l("revenue")}),t?Object.keys(s).length>0?e("ul",{className:"smb-value-list",children:Object.entries(s).map(([o,d])=>e("li",{children:e("strong",{children:bt(d,o)})},o))}):e("p",{children:l("noRevenue")}):e("p",{children:l("revenueLocked")}),t&&e("p",{className:"smb-value-note",children:l("revenueMethod")}),e("p",{className:"smb-value-note",children:l("orderRule",l(We[r.window_hours]||"hours24"),a)})]})}function kt({outcomes:r,sources:t}){const s=[["confirmed",r.resolved_confirmed,r.counts.resolved_confirmed],["inferred",r.resolved_inferred,r.counts.resolved_inferred],["handedOff",r.handed_off,r.counts.handed_off],["otherOutcome",r.unresolved_or_unknown,r.counts.unresolved+r.counts.abandoned+r.counts.unknown]];return e("article",{className:"smb-value-card","aria-labelledby":"smb-outcomes-title",children:[e("h3",{id:"smb-outcomes-title",children:l("outcomesTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.finished)}),r.insufficient_data?e("p",{className:"smb-value-empty",children:l("notEnough",r.finished,30)}):e("dl",{className:"smb-value-rows",children:s.map(([a,o,d])=>e("div",{children:[e("dt",{children:l(a)}),e("dd",{children:[e("strong",{children:o?`${o.rate}%`:"—"})," ",e("span",{className:"smb-value-sub-inline",children:["(",d,")"]}),(o==null?void 0:o.ci95)&&e("span",{className:"smb-value-sub",children:l("ci",o.ci95.low,o.ci95.high)})]})]},a))}),e("p",{className:"smb-value-note",children:l("outcomeNote")}),e("h4",{children:l("bySource")}),e("ul",{className:"smb-value-list smb-value-sources",children:ft.map(([a,o])=>e("li",{children:[e("span",{children:l(o)})," ",e("strong",{children:(t==null?void 0:t[a])??0})]},a))})]})}const Nt=["value","inbox","rules","leads"],He="smbcommerce_admin_tab";function xt(){try{const r=window.localStorage.getItem(He);return r&&Nt.includes(r)?r:"value"}catch{return"value"}}const Ct=()=>{var o,d;const[r,t]=p(xt),s=i=>{t(i);try{window.localStorage.setItem(He,i)}catch{}},a=((d=(o=window.SMBCommerceAdmin)==null?void 0:o.i18n)==null?void 0:d.tab)||"AI Value & Performance";return e("div",{className:"smb-commerce-admin-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Admin"}),e("h2",{className:"nav-tab-wrapper",children:[e("button",{className:`nav-tab ${r==="value"?"nav-tab-active":""}`,onClick:()=>s("value"),"aria-current":r==="value"?"page":void 0,children:a}),e("button",{className:`nav-tab ${r==="inbox"?"nav-tab-active":""}`,onClick:()=>s("inbox"),"aria-current":r==="inbox"?"page":void 0,children:"Inbox"}),e("button",{className:`nav-tab ${r==="rules"?"nav-tab-active":""}`,onClick:()=>s("rules"),"aria-current":r==="rules"?"page":void 0,children:"Rules Engine"}),e("button",{className:`nav-tab ${r==="leads"?"nav-tab-active":""}`,onClick:()=>s("leads"),"aria-current":r==="leads"?"page":void 0,children:"Leads"})]}),e("div",{style:{marginTop:20},children:[r==="value"&&e(yt,{}),r==="inbox"&&e(ct,{}),r==="rules"&&e(ht,{}),r==="leads"&&e(ut,{})]})]})},St=()=>e("div",{className:"smb-commerce-settings-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Settings"}),e(pt,{})]}),Ae=document.getElementById("smb-commerce-admin-root");Ae&&Be(Ae).render(e(Ct,{}));const Le=document.getElementById("smb-commerce-settings-root");Le&&Be(Le).render(e(St,{}));
     4What is your return policy?`,value:t.smbcommerce_suggestion_chips,onChange:s=>n({...t,smbcommerce_suggestion_chips:s.target.value})}),e("p",{className:"description",children:"One suggestion per line. Visitors can tap one to send that question instantly."})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-network"})," AI configuration"]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Assistant mode"}),e("select",{value:t.smbcommerce_runtime_mode,onChange:s=>n({...t,smbcommerce_runtime_mode:s.target.value}),children:[e("option",{value:"smart_hybrid",children:"Smart hybrid — answer locally where possible, use AI when needed (recommended)"}),e("option",{value:"local_only",children:"Local only — never send conversations to an AI provider"}),e("option",{value:"ai_enhanced",children:"AI enhanced — let the AI lead every reply"})]}),e("p",{className:"description",children:"Local only answers greetings, policy questions, cart and order lookups from your own store data and costs no tokens. It cannot answer anything else — those customers are asked to contact you directly. This setting covers AI providers only; Telegram notifications, webhooks and licensing still reach the internet."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-reply-language",children:"Reply language"}),e("select",{id:"smb-reply-language",value:t.smbcommerce_reply_language,onChange:s=>n({...t,smbcommerce_reply_language:s.target.value}),children:[e("option",{value:"customer",children:"The customer's language (recommended)"}),e("option",{value:"store",children:"As in earlier versions: follow the sales playbook and site language"})]}),e("p",{className:"description",children:"With the customer's language, the assistant replies in the language each customer writes in and keeps product names, prices and order numbers exactly as they are in your store. Fixed messages, such as the handoff confirmation, follow the customer's language when this site has a translation for it."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI provider"}),e("select",{value:t.smbcommerce_ai_provider,onChange:s=>n({...t,smbcommerce_ai_provider:s.target.value}),children:[e("option",{value:"openai",children:"OpenAI (GPT-4o-mini)"}),e("option",{value:"anthropic",children:"Anthropic (Claude 3.5)"}),e("option",{value:"gemini",children:"Google Gemini"}),(K||t.smbcommerce_ai_provider==="wordpress")&&e("option",{value:"wordpress",children:"WordPress AI connection (Settings → Connectors)"})]}),t.smbcommerce_ai_provider==="wordpress"&&e("p",{className:"description",children:K?"Uses the AI provider you set up in Settings → Connectors. The key stays in WordPress; this plugin never reads it. The keys below are not needed for this option.":"No AI provider is set up in Settings → Connectors any more. Set one up there, or choose another provider here."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI API Key"}),e("input",{type:"password",placeholder:p.openai?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_openai_key,onChange:s=>n({...t,smbcommerce_openai_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Anthropic API Key"}),e("input",{type:"password",placeholder:p.anthropic?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_anthropic_key,onChange:s=>n({...t,smbcommerce_anthropic_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini API Key"}),e("input",{type:"password",placeholder:p.gemini?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_gemini_key,onChange:s=>n({...t,smbcommerce_gemini_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini model"}),e("input",{type:"text",placeholder:"Leave empty for gemini-flash-latest",value:t.smbcommerce_gemini_model||"",onChange:s=>n({...t,smbcommerce_gemini_model:s.target.value})}),e("p",{className:"description",children:["Leave empty to always use Google's current Flash model. Enter a specific model (for example ",e("code",{children:"gemini-3.5-flash"}),") only if you need it."]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Claude Model"}),e("input",{type:"text",placeholder:"Leave empty for claude-sonnet-5-5",value:t.smbcommerce_anthropic_model,onChange:s=>n({...t,smbcommerce_anthropic_model:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI Model"}),e("input",{type:"text",placeholder:"Leave empty for gpt-4o-mini",value:t.smbcommerce_openai_model,onChange:s=>n({...t,smbcommerce_openai_model:s.target.value})})]}),W&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Models in use"}),["anthropic","openai","gemini"].filter(s=>{var a;return(a=W[s])==null?void 0:a.retired}).map(s=>e("div",{className:"notice notice-warning inline",children:e("p",{children:["The saved ",s==="anthropic"?"Claude":s==="openai"?"OpenAI":"Gemini"," model ",e("code",{children:W[s].configured})," is no longer available from the provider. The assistant is using ",e("code",{children:W[s].model})," instead. Enter a current model above to choose for yourself."]})},s)),((he=W[t.smbcommerce_ai_provider])==null?void 0:he.source)==="default"&&e("div",{className:"notice notice-info inline",children:e("p",{children:["No model is chosen for your main provider, so the assistant uses the default ",e("code",{children:W[t.smbcommerce_ai_provider].model}),". Choosing a model yourself keeps your cost predictable when defaults change."]})}),e("table",{className:"widefat striped",style:{marginTop:8},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Provider"}),e("th",{children:"Model used"}),e("th",{children:"Estimated cost per conversation"})]})}),e("tbody",{children:["anthropic","openai","gemini"].map(s=>{const a=W[s];return a?e("tr",{children:[e("td",{children:[s==="anthropic"?"Claude":s==="openai"?"OpenAI":"Gemini",t.smbcommerce_ai_provider===s?" (main)":""]}),e("td",{children:[e("code",{children:a.model})," ",a.source==="default"?"(default)":a.source==="replacement"?"(replacement)":""]}),e("td",{children:a.estimate_per_conversation===null?"No price on file":`about $${Number(a.estimate_per_conversation).toFixed(4)}`})]},s):null})})]}),e("p",{className:"description",children:"Estimate for a typical conversation: 2 replies with about 7,000 input and 80 output tokens each, at the provider's list price. Your real cost is recorded per request on the AI Value & Performance tab."})]})]}),S&&S.managed&&e("div",{className:"smb-commerce-card",style:{borderLeft:`4px solid ${S.is_paid?"#1b7a51":"#9a5c06"}`},children:[e("h3",{children:[e("span",{className:"dashicons dashicons-awards"})," Licence"]}),e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:200},children:"Current plan"}),e("td",{children:e("strong",{children:S.label})})]}),e("tr",{children:[e("td",{children:"Valid until"}),e("td",{children:S.is_lifetime?"Lifetime, never expires":S.expires_at||"—"})]})]})}),!S.is_paid&&e("p",{className:"description",style:{marginBottom:10},children:"These features need a paid plan: business analytics, AI budget caps, webhooks, lead scoring, provider failover, response cache, ungrounded-figure blocking and product variation tools. Chat, product sync, the sales playbook and lead capture keep working as normal."}),S.tier==="starter"&&e("p",{className:"description",style:{marginBottom:10},children:"The Pro plan adds provider failover, AI budget caps, coupon validation, lead scoring, advanced analytics, price verification and outgoing webhooks."}),S.upgrade_url&&S.tier!=="pro"&&e("a",{className:"button button-primary",href:S.upgrade_url,children:"View pricing"}),S.account_url&&e("a",{className:"button",style:{marginLeft:8},href:S.account_url,children:"Manage licence"})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-chart-bar"})," Business performance"]}),e("p",{className:"description",style:{marginTop:0},children:["Last ",B," days."," ",[7,30,90].map(s=>e("button",{type:"button",className:"button button-small",style:{marginRight:4,fontWeight:B===s?700:400},onClick:()=>P(s),children:[s," days"]},s))]}),l&&l.locked?e("p",{className:"description",children:"A paid plan is required to view analytics. Data is still being recorded — upgrade and your full history appears immediately."}):l&&l.summary?e("div",{children:[e("table",{className:"widefat striped",style:{marginBottom:14},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:260},children:"Conversations"}),e("td",{children:e("strong",{children:l.summary.conversations})})]}),e("tr",{children:[e("td",{children:"Leads captured"}),e("td",{children:[e("strong",{children:l.summary.leads})," (",l.summary.lead_rate,"% of conversations) · ",l.summary.hot_leads," hot"]})]}),e("tr",{children:[e("td",{children:"Not handed off to staff"}),e("td",{children:[e("strong",{children:[l.summary.resolution_rate,"%"]})," · handed off ",l.summary.handoff_rate,"%"]})]}),e("tr",{children:[e("td",{children:"AI cost (estimated)"}),e("td",{children:e("strong",{children:V(l.summary.cost_usd)})})]}),e("tr",{children:[e("td",{children:"Cost per conversation"}),e("td",{children:V(l.summary.cost_per_conversation)})]}),e("tr",{children:[e("td",{children:"Cost per lead"}),e("td",{children:l.summary.leads>0?V(l.summary.cost_per_lead):"—"})]}),e("tr",{children:[e("td",{children:"AI calls · average latency"}),e("td",{children:[l.summary.ai_calls," calls · ",l.summary.avg_latency_ms,"ms · ",l.summary.error_rate,"% errors"]})]}),e("tr",{children:[e("td",{children:"Tokens used"}),e("td",{children:(l.summary.prompt_tokens+l.summary.completion_tokens).toLocaleString()})]})]})}),l.budget&&(l.budget.daily_limit>0||l.budget.monthly_limit>0)&&e("p",{className:"description",children:[l.budget.daily_limit>0&&e("span",{children:["Today: ",V(l.budget.daily_spent)," / ",V(l.budget.daily_limit)," (",l.budget.daily_percent,"%). "]}),l.budget.monthly_limit>0&&e("span",{children:["This month: ",V(l.budget.monthly_spent)," / ",V(l.budget.monthly_limit)," (",l.budget.monthly_percent,"%)."]})]}),l.cache&&l.cache.hits>0&&e("p",{className:"description",children:["Cache: ",l.cache.entries," entries stored, ",l.cache.saved_calls," model calls saved."]}),l.handoffs&&l.handoffs.length>0&&e("div",{style:{marginTop:14},children:[e("strong",{style:{fontSize:13},children:"Recently handed off to a human"}),e("p",{className:"description",style:{marginTop:2},children:"This is where the bot ran out of knowledge — worth reading to fill the gaps."}),e("table",{className:"widefat striped",children:e("tbody",{children:l.handoffs.map(s=>e("tr",{children:[e("td",{style:{width:150},children:s.updated_at}),e("td",{style:{width:70},children:s.score}),e("td",{children:s.intent||s.summary||"—"})]},s.id))})})]})]}):e("p",{className:"description",children:"No data yet. Figures appear after the first conversation."})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-shield"})," Cost & reliability"]}),!(y("budget")&&y("provider_failover")&&y("output_guard"))&&e("p",{className:"description",children:["AI budget caps, provider failover and price verification need the Pro plan.",!y("response_cache")&&" The response cache needs the Starter plan."]}),y("budget")&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI budget cap (USD)"}),e("div",{style:{display:"flex",gap:10,flexWrap:"wrap"},children:[e("input",{type:"number",min:"0",step:"0.5",style:{maxWidth:150},placeholder:"Per day",value:t.smbcommerce_budget_daily_usd,onChange:s=>n({...t,smbcommerce_budget_daily_usd:s.target.value})}),e("input",{type:"number",min:"0",step:"1",style:{maxWidth:150},placeholder:"Per month",value:t.smbcommerce_budget_monthly_usd,onChange:s=>n({...t,smbcommerce_budget_monthly_usd:s.target.value})})]}),e("p",{className:"description",children:["Set to 0 for no limit. When the cap is reached the bot stops replying and hands the conversation to your team; the visitor never sees anything about billing. Cost is an",e("strong",{children:" estimate"})," based on published rates, not your actual invoice."]})]}),y("provider_failover")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_provider_fallback==="1",onChange:s=>n({...t,smbcommerce_enable_provider_fallback:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Automatic provider failover"}),e("p",{className:"description",style:{margin:0},children:"If the primary provider fails or is rate limited, try another provider that already has an API key instead of showing the visitor an error."})]})]}),y("output_guard")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_block_ungrounded_prices==="1",onChange:s=>n({...t,smbcommerce_block_ungrounded_prices:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Block replies containing ungrounded figures"}),e("p",{className:"description",style:{margin:0},children:"Every price figure in a reply is checked against the real data given to the AI. When off, mismatches are still logged but not blocked — turn this on once the log shows few false positives."})]})]}),y("response_cache")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_response_cache==="1",onChange:s=>n({...t,smbcommerce_enable_response_cache:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Cache repeated questions"}),e("p",{className:"description",style:{margin:0},children:["Identical questions reuse the previous answer. Questions about price, stock, orders and coupons are never cached.",e("input",{type:"number",min:"1",style:{width:80,marginLeft:8},value:t.smbcommerce_cache_ttl_hours,onChange:s=>n({...t,smbcommerce_cache_ttl_hours:s.target.value})})," hours",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:X,children:"Clear cache"})]})]})]})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," Outgoing webhooks"]}),y("webhooks")?e(D,{children:[e("p",{className:"description",style:{marginTop:0},children:["Send events to n8n, Make, Zapier or your CRM. Events:"," ",e("code",{children:"lead.created"})," ",e("code",{children:"lead.qualified"})," ",e("code",{children:"human.requested"})," ",e("code",{children:"budget.exceeded"})," ",e("code",{children:"ai.failed"})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Webhook URL"}),e("input",{type:"url",placeholder:"https://n8n.example.com/webhook/...",value:t.smbcommerce_webhook_url,onChange:s=>n({...t,smbcommerce_webhook_url:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Signing secret"}),e("input",{type:"password",placeholder:t.smbcommerce_webhook_secret_exists?"•••••••••• (saved)":"Any secret string",value:t.smbcommerce_webhook_secret,onChange:s=>n({...t,smbcommerce_webhook_secret:s.target.value})}),e("p",{className:"description",children:["With a secret set, every request carries the header ",e("code",{children:"X-SMB-Commerce-Signature: sha256=..."}),", computed with HMAC-SHA256 over the body. The receiver verifies it to confirm the request came from this site."]})]}),e("button",{type:"button",className:"button",onClick:z,children:"Send test"}),O&&e("p",{className:"description",style:{marginTop:8},children:O.pending?"Sending...":O.success?`Sent. The receiver returned HTTP ${O.status}${O.signed?", signed":", unsigned"}.`:`Error: ${O.message||"unknown error"}`})]}):e("p",{className:"description",style:{marginTop:0},children:"Outgoing webhooks are available in the Pro plan."})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-megaphone"})," Sales playbook"]}),e("p",{className:"description",style:{marginTop:0},children:["This is what decides whether the bot actually sells. Available variables:",e("code",{children:"{brand}"})," ",e("code",{children:"{hotline}"})," ",e("code",{children:"{zalo}"})," ",e("code",{children:"{messenger}"}),"."]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Main playbook"}),e("textarea",{rows:16,style:{fontFamily:"monospace",fontSize:"12px"},value:t.smbcommerce_sales_playbook,onChange:s=>n({...t,smbcommerce_sales_playbook:s.target.value})}),e("p",{className:"description",children:["Role, tone, sales flow, objection handling and how to ask for contact details.",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:Te,children:"Restore default playbook"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Business information"}),e("textarea",{rows:6,style:{fontFamily:"monospace",fontSize:"12px"},placeholder:"Business hours, payment, delivery, returns...",value:t.smbcommerce_business_info,onChange:s=>n({...t,smbcommerce_business_info:s.target.value})}),o!==null&&t.smbcommerce_business_info===o&&e("div",{className:"notice notice-warning inline",children:e("p",{children:"This is the sample text added by an earlier version, not your store's details. The assistant does not use it. Replace it with your real opening hours, payment, delivery and return terms, then click Save."})}),e("p",{className:"description",children:"The bot may answer policy questions only from this text. Leave it empty and the bot hands off to your team instead of guessing."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:["Detailed policies (",e("code",{children:"get_policy"}),")"]}),e("p",{className:"description",style:{marginTop:0},children:"Any field left empty falls back to the Business information above for that policy type."}),[["smbcommerce_refund_policy","Refunds"],["smbcommerce_warranty_policy","Warranty"],["smbcommerce_delivery_policy","Delivery"],["smbcommerce_privacy_policy","Privacy"]].map(([s,a])=>e("div",{style:{marginBottom:10},children:[e("label",{style:{fontWeight:500},children:a}),e("textarea",{rows:3,value:t[s],onChange:E=>n({...t,[s]:E.target.value})})]},s))]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_adaptive_playbook==="1",onChange:s=>n({...t,smbcommerce_enable_adaptive_playbook:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Situational guidance"}),e("p",{className:"description",style:{margin:0},children:"Detects whether the visitor is asking about price, pushing back on cost, hesitating, comparing or ready to buy, then adds guidance for that specific turn. Also reminds the bot to ask for contact details after a few exchanges."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-products"})," Product data"]}),e("p",{className:"description",style:{marginTop:0},children:"The bot can only recommend products that have been synced into its cache. Without a sync it has nothing to sell."}),v&&e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:220},children:"WooCommerce"}),e("td",{children:v.woocommerce_active?"Active":"NOT ACTIVE"})]}),e("tr",{children:[e("td",{children:"Products in cache"}),e("td",{children:e("strong",{children:v.total_cached})})]}),e("tr",{children:[e("td",{children:"Last sync"}),e("td",{children:v.last_sync||"Never"})]}),e("tr",{children:[e("td",{children:"Next scheduled run"}),e("td",{children:v.next_scheduled_sync||"Not scheduled"})]})]})}),e("button",{type:"button",className:"button",onClick:Le,disabled:C,children:C?"Syncing...":"Sync products now"}),L&&e("p",{className:"description",style:{marginTop:8},children:L.success?`Synced ${L.synced} products. Total in cache: ${L.total_cached}.`:`Error: ${L.message||L.error||"unknown error"}`})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-cart"})," Cart in the chat"]}),e("p",{className:"description",style:{marginTop:0},children:"Product cards in the chat have an Add to cart button and the chat shows the customer's cart. Everything goes through your store's own cart, so prices, stock and coupon rules are checked by WooCommerce as usual. The assistant never changes a cart by itself."}),y("cart_actions_advanced")?e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-coupon-allowlist",children:"Discount codes the assistant may offer"}),e("textarea",{id:"smb-coupon-allowlist",rows:4,placeholder:`WELCOME10, 10
     5FREESHIP`,value:t.smbcommerce_coupon_allowlist,onChange:s=>n({...t,smbcommerce_coupon_allowlist:s.target.value})}),e("p",{className:"description",children:"One coupon code per line, up to 20. Add a comma and a number to cap the discount the assistant may offer: a percentage for percentage coupons, an amount for fixed coupons. The assistant offers a code only when a customer asks about discounts or hesitates over the price, and shows an Apply button. Codes that are not listed here are never offered. Leave it empty to turn this off."})]}):e("p",{className:"description",id:"smb-commerce-cart-plan-note",children:"Choosing product options on the card, changing quantities, removing items and applying discount codes in the chat are available in the Starter plan."})]}),se&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," AI agents (Abilities API / MCP)"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_abilities_enabled==="1",onChange:s=>n({...t,smbcommerce_abilities_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let AI agents read your public catalog"}),e("p",{className:"description",style:{margin:0},children:"Adds three read-only abilities to WordPress: search products, product details and store policies. With the MCP Adapter plugin installed, AI assistants and agents can use them as tools. Only published, visible products and your policy texts are shared; never orders, carts or customer data. Limited to 60 requests per hour per visitor. Off by default."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-location"})," Order tracking"]}),e("p",{className:"description",style:{marginTop:0},children:"When a customer asks about an order, the chat shows an order card with its status and tracking number. Tracking numbers saved by WooCommerce Shipment Tracking or Advanced Shipment Tracking appear automatically. If your shipping plugin saves the tracking number in its own order field, enter that field's meta key below. Logged-in customers can ask about their orders without an order number."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-code-meta",children:"Tracking number meta key"}),e("input",{id:"smb-tracking-code-meta",type:"text",placeholder:"e.g. _ghn_order_code",value:t.smbcommerce_tracking_code_meta,onChange:s=>n({...t,smbcommerce_tracking_code_meta:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-carrier-meta",children:"Carrier meta key (optional)"}),e("input",{id:"smb-tracking-carrier-meta",type:"text",value:t.smbcommerce_tracking_carrier_meta,onChange:s=>n({...t,smbcommerce_tracking_carrier_meta:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-carrier",children:"Carrier name (optional)"}),e("input",{id:"smb-tracking-carrier",type:"text",placeholder:"e.g. GHN",value:t.smbcommerce_tracking_carrier,onChange:s=>n({...t,smbcommerce_tracking_carrier:s.target.value})}),e("p",{className:"description",children:"Used when there is no carrier meta key, or it is empty on an order."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-url",children:"Tracking link (optional)"}),e("input",{id:"smb-tracking-url",type:"url",placeholder:"https://carrier.example/track?code={code}",value:t.smbcommerce_tracking_url_template,onChange:s=>n({...t,smbcommerce_tracking_url_template:s.target.value})}),e("p",{className:"description",children:["A web address with ","{code}"," where the tracking number goes. The chat shows a Track button only when this is set. Meta keys may use letters, numbers, _ and -."]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-tools"})," Advanced features"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_link_reading==="1",onChange:s=>n({...t,smbcommerce_enable_link_reading:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Let the bot read links visitors send"}),e("p",{className:"description",style:{margin:0},children:["When enabled, every URL a visitor pastes into the chat is sent to the external service ",e("code",{children:"r.jina.ai"})," to fetch its content. Leave this off if visitors might paste private or internal links. Off by default."]})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Maximum upload size (MB)"}),e("input",{type:"number",value:t.smbcommerce_max_upload_size,onChange:s=>n({...t,smbcommerce_max_upload_size:s.target.value}),min:"1",max:"50"}),e("p",{className:"description",children:"5MB is recommended to avoid filling up server storage."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"File retention (days)"}),e("input",{type:"number",value:t.smbcommerce_media_retention_days,onChange:s=>n({...t,smbcommerce_media_retention_days:s.target.value}),min:"1",max:"365"}),e("p",{className:"description",children:'Files (images, documents) in CLOSED conversations older than this are deleted automatically to keep the server clean. Their links are replaced with an "Expired" notice.'})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"System guardrails prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"e.g. You are a customer support AI. Never disclose sensitive information...",value:t.smbcommerce_system_guardrails_prompt,onChange:s=>n({...t,smbcommerce_system_guardrails_prompt:s.target.value})}),e("p",{className:"description",children:"Highest-priority guardrail, always pinned at the top of the context to resist jailbreaks and hallucination."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Sales & CSKH Flow Prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"Enter your sales playbook here...",value:t.smbcommerce_meta_prompt_template,onChange:s=>n({...t,smbcommerce_meta_prompt_template:s.target.value})})]}),y("telegram")?e(D,{children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram bot token"}),e("input",{type:"password",placeholder:p.telegram?"•••••••••••• (saved)":"Enter the bot token from BotFather...",value:t.smbcommerce_telegram_bot_token,onChange:s=>n({...t,smbcommerce_telegram_bot_token:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram Chat ID"}),e("input",{type:"text",placeholder:"e.g. -1001234567890",value:t.smbcommerce_telegram_chat_id,onChange:s=>n({...t,smbcommerce_telegram_chat_id:s.target.value})}),e("p",{className:"description",children:"Without a Chat ID new-lead notifications cannot be delivered, even with a Bot Token set."})]})]}):e("p",{className:"description",id:"smb-commerce-telegram-plan-note",children:"Telegram notifications are available in the Starter plan."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Handoff Telegram Webhook URL"}),e("input",{type:"text",placeholder:"https://...",value:t.smbcommerce_handoff_webhook,onChange:s=>n({...t,smbcommerce_handoff_webhook:s.target.value})}),e("p",{className:"description",children:"URL that receives an urgent notification whenever a handoff is triggered."})]})]}),e("div",{className:"smb-commerce-actions",children:[e("button",{type:"submit",className:"button button-primary button-large",disabled:u,children:u?"Saving...":"Save all settings"}),e("button",{type:"button",className:"button button-secondary",onClick:Re,disabled:_,style:{marginLeft:"10px"},children:_?"Testing...":"Test AI connection"})]}),x&&e("div",{style:{marginTop:"16px",padding:"14px 18px",borderRadius:"8px",background:x.error?"#fff0f0":(ue=x.test_result)!=null&&ue.startsWith("OK")?"#f0fff4":"#fffbe6",border:`1px solid ${x.error?"#ffcdd2":(pe=x.test_result)!=null&&pe.startsWith("OK")?"#a7f3d0":"#fde68a"}`,fontSize:"13px",lineHeight:"1.7"},children:x.error?e("div",{children:[e("strong",{children:"Error:"})," ",x.error]}):e(D,{children:[e("div",{children:[e("strong",{children:"Selected provider:"})," ",x.configured_provider]}),e("div",{children:[e("strong",{children:"Class:"})," ",x.provider_class]}),e("div",{children:[e("strong",{children:"Gemini key:"})," ",x.has_gemini_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"OpenAI key:"})," ",x.has_openai_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"Claude key:"})," ",x.has_claude_key?"Saved":"Not set"]}),e("div",{style:{marginTop:"8px",padding:"8px",background:"#fff",borderRadius:"6px",border:"1px solid #e5e7eb"},children:[e("strong",{children:"Test result:"}),e("br",{}),e("code",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:x.test_result||"No provider"})]})]})})]})}),e("div",{className:"smb-commerce-settings-preview-col",children:e("div",{className:"preview-sticky",children:[e("h3",{children:"Live preview"}),e("p",{className:"description",children:"How the widget will look on your site."}),e("div",{className:"widget-preview-box",style:t.smbcommerce_font_family&&t.smbcommerce_font_family!=="system"?{fontFamily:`'${t.smbcommerce_font_family}', sans-serif`}:void 0,children:[e("div",{className:"widget-preview-header",style:{backgroundColor:t.smbcommerce_primary_color},children:[e("div",{className:"widget-preview-header-brand",children:[t.smbcommerce_logo_url?e("img",{src:t.smbcommerce_logo_url,alt:"Logo",className:"widget-preview-logo"}):e("svg",{viewBox:"0 0 24 24",width:"24",height:"24",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),e("span",{children:t.smbcommerce_brand_name||"Brand name"})]}),e("div",{className:"widget-preview-close",children:"×"})]}),e("div",{className:"widget-preview-body",children:[e("div",{className:"preview-message bot",children:"Hello! How can I help you today?"}),e("div",{className:"preview-message user",style:{backgroundColor:t.smbcommerce_primary_color,color:"#fff"},children:"I'd like to ask about your services."})]}),e("div",{className:"widget-preview-footer",children:[e("button",{style:{background:"transparent",border:"none",color:"#666",padding:"0 8px"},children:e("svg",{viewBox:"0 0 24 24",width:"20",height:"20",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"})})}),e("input",{type:"text",placeholder:"Type a message...",disabled:!0}),e("button",{style:{backgroundColor:t.smbcommerce_primary_color},children:e("svg",{viewBox:"0 0 24 24",width:"16",height:"16",stroke:"currentColor",strokeWidth:"2",fill:"none",children:[e("line",{x1:"22",y1:"2",x2:"11",y2:"13"}),e("polygon",{points:"22 2 15 22 11 13 2 9 22 2"})]})})]})]})]})})]})]})}var we;const Ge=((we=window.SMBCommerceAdmin)==null?void 0:we.i18n)||{};function c(r,...t){let n=0;return(Ge[r]||r).replace(/%(\d+)\$s/g,(p,m)=>String(t[Number(m)-1]??"")).replace(/%s/g,()=>String(t[n++]??"")).replace(/%%/g,"%")}const j=r=>{const t=Number(r)||0,n=t<0?"−":"",p=Math.abs(t);return`${n}$${p.toFixed(p>0&&p<1?4:2)}`},Ve=(r,t)=>{try{return new Intl.NumberFormat(void 0,{style:"currency",currency:t}).format(r)}catch{return`${r.toFixed(2)} ${t}`}},Je=r=>r?new Date(r.replace(" ","T")+"Z").toLocaleDateString():"",Ce={24:"hours24",72:"hours72",168:"days7"},ne={processing:"statusProcessing",completed:"statusCompleted","on-hold":"statusOnHold"},ae={ai_conversation:"unitAiConversation",resolved_conversation:"unitResolved",month:"unitMonth"},Ke=[["ai","sourceAi"],["rule","sourceRule"],["customer","sourceCustomer"],["agent","sourceAgent"],["budget","sourceBudget"]];function Ye(){const{notices:r,push:t,dismiss:n}=oe(),[p,m]=d(30),[u,k]=d(null),[h,g]=d(!0),[x,T]=d(!1),[_,N]=d(null),[C,I]=d(!1),L=async i=>{g(!0),T(!1);try{const l=await w.get(`/admin/value-metrics?days=${i}`);k(l),N(R=>R??l.settings)}catch{T(!0)}finally{g(!1)}};F(()=>{L(p)},[p]);const $=async i=>{if(i.preventDefault(),!!_){I(!0);try{const l=await w.post("/admin/value-settings",_);N(l),t("success",c("saved")),L(p)}catch{t("error",c("saveError"))}finally{I(!1)}}},v=(i,l)=>N(R=>R&&{...R,reference:{...R.reference,[i]:l}});return e("section",{className:"smb-value","aria-labelledby":"smb-value-title",children:[e(ie,{notices:r,onDismiss:n}),e("div",{className:"smb-value-header",children:[e("div",{children:[e("h2",{id:"smb-value-title",children:c("tab")}),e("p",{className:"smb-value-intro",children:c("intro")}),u&&e("p",{className:"smb-value-meta",children:u.measurement_started_at?c("measuredSince",Je(u.measurement_started_at)):c("notMeasured")})]}),e("label",{className:"smb-value-period",children:[e("span",{children:c("period")}),e("select",{value:p,onChange:i=>m(Number(i.target.value)),children:[7,30,90].map(i=>e("option",{value:i,children:c("lastDays",i)},i))})]})]}),h&&e("p",{role:"status",className:"smb-value-status",children:c("loading")}),x&&e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:c("loadError")})}),u&&!x&&e("div",{className:"smb-value-grid","aria-busy":h,children:[e(Ze,{cost:u.cost}),e(Qe,{orders:u.orders,allowed:u.revenue_allowed}),e(Xe,{outcomes:u.outcomes,sources:u.handoffs_by_source})]}),_&&e("details",{className:"smb-value-settings",children:[e("summary",{children:c("settingsTitle")}),e("form",{onSubmit:$,children:[e("fieldset",{children:[e("legend",{children:c("costTitle")}),e("p",{className:"description",children:c("refHelp")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[c("refLabel"),e("input",{type:"text",maxLength:80,value:_.reference.label,onChange:i=>v("label",i.target.value)})]}),e("label",{children:[c("refUnit"),e("select",{value:_.reference.unit,onChange:i=>v("unit",i.target.value),children:Object.keys(ae).map(i=>e("option",{value:i,children:c(ae[i])},i))})]}),e("label",{children:[c("refPrice"),e("input",{type:"number",min:"0",step:"0.0001",value:_.reference.price,onChange:i=>v("price",Number(i.target.value))})]}),e("label",{children:[c("refSource"),e("input",{type:"text",maxLength:200,value:_.reference.source,onChange:i=>v("source",i.target.value)})]}),e("label",{children:[c("refDate"),e("input",{type:"date",value:_.reference.date,onChange:i=>v("date",i.target.value)})]}),e("label",{children:[c("pluginMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:_.plugin_cost_monthly,onChange:i=>N({..._,plugin_cost_monthly:Number(i.target.value)})})]}),e("label",{children:[c("otherMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:_.other_cost_monthly,onChange:i=>N({..._,other_cost_monthly:Number(i.target.value)})})]})]})]}),e("fieldset",{children:[e("legend",{children:c("ordersTitle")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[c("window"),e("select",{value:_.window_hours,onChange:i=>N({..._,window_hours:Number(i.target.value)}),"aria-describedby":"smb-window-help",children:[24,72,168].map(i=>e("option",{value:i,children:c(Ce[i])},i))}),e("span",{id:"smb-window-help",className:"description",children:c("windowHelp")})]}),e("div",{role:"group","aria-labelledby":"smb-statuses-label",children:[e("span",{id:"smb-statuses-label",className:"smb-value-group-label",children:c("statuses")}),Object.keys(ne).map(i=>e("label",{className:"smb-value-check",children:[e("input",{type:"checkbox",checked:_.statuses.includes(i),onChange:l=>N({..._,statuses:l.target.checked?[..._.statuses,i]:_.statuses.filter(R=>R!==i)})}),c(ne[i])]},i))]})]})]}),e("button",{type:"submit",className:"button button-primary",disabled:C,children:c(C?"saving":"save")})]})]})]})}function Ze({cost:r}){const t=r.reference;return e("article",{className:"smb-value-card","aria-labelledby":"smb-cost-title",children:[e("h3",{id:"smb-cost-title",children:c("costTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.ai_conversations)}),e("dl",{className:"smb-value-rows",children:[e("div",{children:[e("dt",{children:c("aiCost")}),e("dd",{children:[e("strong",{children:j(r.ai_cost_total)}),e("span",{className:"smb-value-sub",children:c("aiCostSplit",j(r.ai_cost_usage),j(r.ai_cost_estimated))}),r.unpriced_requests>0&&e("span",{className:"smb-value-sub",children:c("unpriced",r.unpriced_requests)})]})]}),e("div",{children:[e("dt",{children:c("pluginCost")}),e("dd",{children:j(r.plugin_cost)})]}),e("div",{children:[e("dt",{children:c("otherCost")}),e("dd",{children:j(r.other_cost)})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:c("totalCost")}),e("dd",{children:e("strong",{children:j(r.total_cost)})})]}),t&&e(D,{children:[e("div",{children:[e("dt",{children:c("referenceCost",t.label)}),e("dd",{children:[j(t.cost),e("span",{className:"smb-value-sub",children:[c("referenceVolume",t.volume,j(t.price))," ",c(ae[t.unit]||"unitAiConversation")]}),t.source&&e("span",{className:"smb-value-sub",children:[c("referenceSource",t.source),t.date?` · ${t.date}`:""]})]})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:c("difference")}),e("dd",{children:e("strong",{children:j(t.difference)})})]})]})]}),e("p",{className:"smb-value-note",children:c(t?"differenceNote":"noReference")}),e("p",{className:"smb-value-note",children:c("costScope")})]})}function Qe({orders:r,allowed:t}){const n=r.revenue_by_currency||{},p=(r.statuses||[]).map(m=>c(ne[m]||m)).join(", ");return e("article",{className:"smb-value-card","aria-labelledby":"smb-orders-title",children:[e("h3",{id:"smb-orders-title",children:c("ordersTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.assisted_orders)}),e("p",{className:"smb-value-big",children:[e("strong",{children:r.assisted_orders})," ",e("span",{children:c("orders")})]}),e("p",{className:"smb-value-caption",children:c("assistedCaption")}),e("h4",{children:c("revenue")}),t?Object.keys(n).length>0?e("ul",{className:"smb-value-list",children:Object.entries(n).map(([m,u])=>e("li",{children:e("strong",{children:Ve(u,m)})},m))}):e("p",{children:c("noRevenue")}):e("p",{children:c("revenueLocked")}),t&&e("p",{className:"smb-value-note",children:c("revenueMethod")}),e("p",{className:"smb-value-note",children:c("orderRule",c(Ce[r.window_hours]||"hours24"),p)})]})}function Xe({outcomes:r,sources:t}){const n=[["confirmed",r.resolved_confirmed,r.counts.resolved_confirmed],["inferred",r.resolved_inferred,r.counts.resolved_inferred],["handedOff",r.handed_off,r.counts.handed_off],["otherOutcome",r.unresolved_or_unknown,r.counts.unresolved+r.counts.abandoned+r.counts.unknown]];return e("article",{className:"smb-value-card","aria-labelledby":"smb-outcomes-title",children:[e("h3",{id:"smb-outcomes-title",children:c("outcomesTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.finished)}),r.insufficient_data?e("p",{className:"smb-value-empty",children:c("notEnough",r.finished,30)}):e("dl",{className:"smb-value-rows",children:n.map(([p,m,u])=>e("div",{children:[e("dt",{children:c(p)}),e("dd",{children:[e("strong",{children:m?`${m.rate}%`:"—"})," ",e("span",{className:"smb-value-sub-inline",children:["(",u,")"]}),(m==null?void 0:m.ci95)&&e("span",{className:"smb-value-sub",children:c("ci",m.ci95.low,m.ci95.high)})]})]},p))}),e("p",{className:"smb-value-note",children:c("outcomeNote")}),e("h4",{children:c("bySource")}),e("ul",{className:"smb-value-list smb-value-sources",children:Ke.map(([p,m])=>e("li",{children:[e("span",{children:c(m)})," ",e("strong",{children:(t==null?void 0:t[p])??0})]},p))})]})}const et=1,tt="./admin-insights.bundle.js";let ee=null;function st(){if(!ee){const r=new URL(import.meta.url);ee=import(new URL(tt+r.search,r).href).then(n=>{if(!n||n.API!==et)throw new Error("Sales Insights module mismatch");return n}).catch(n=>{throw ee=null,n})}return ee}const ve=["value","insights","inbox","rules","leads"],Se="smbcommerce_admin_tab";function rt(){const r=new URLSearchParams(window.location.search).get("smb_tab");if(r&&ve.includes(r))return r;try{const t=window.localStorage.getItem(Se);return t&&ve.includes(t)?t:"value"}catch{return"value"}}const nt=()=>{var C,I,L,$;const[r,t]=d(rt),[n,p]=d(null),[m,u]=d(!1),[k,h]=d(null),g=v=>{t(v);try{window.localStorage.setItem(Se,v)}catch{}},x=((I=(C=window.SMBCommerceAdmin)==null?void 0:C.i18n)==null?void 0:I.tab)||"AI Value & Performance",T=(($=(L=window.SMBCommerceAdmin)==null?void 0:L.i18n)==null?void 0:$.insightsTab)||"Sales Insights";F(()=>{r!=="insights"||n||(u(!1),st().then(v=>p(()=>v)).catch(()=>u(!0)))},[r,n]);const _=v=>{h(v),g("inbox")},N=(v,i)=>e("button",{className:`nav-tab ${r===v?"nav-tab-active":""}`,onClick:()=>g(v),"aria-current":r===v?"page":void 0,children:i});return e("div",{className:"smb-commerce-admin-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Admin"}),e("h2",{className:"nav-tab-wrapper",children:[N("value",x),N("insights",T),N("inbox","Inbox"),N("rules","Rules Engine"),N("leads","Leads")]}),e("div",{style:{marginTop:20},children:[r==="value"&&e(Ye,{}),r==="insights"&&(n?e(n.SalesInsights,{onOpenConversation:_}):m?e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:be("loadError")})}):e("p",{role:"status",className:"smb-value-status",children:be("loading")})),r==="inbox"&&e(De,{openId:k}),r==="rules"&&e(je,{}),r==="leads"&&e(Ue,{})]})]})},at=()=>e("div",{className:"smb-commerce-settings-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Settings"}),e(qe,{})]}),fe=document.getElementById("smb-commerce-admin-root");fe&&Ne(fe).render(e(nt,{}));const ke=document.getElementById("smb-commerce-settings-root");ke&&Ne(ke).render(e(at,{}));
  • ninhle-ai-sales-assistant/tags/2.6.0/assets/dist/admin.css

    r3752682 r3765552  
    1 .smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{margin:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.smb-commerce-settings-layout{display:grid;grid-template-columns:1fr 380px;gap:20px;align-items:start}@media (max-width: 1024px){.smb-commerce-settings-layout{grid-template-columns:1fr}}.smb-commerce-settings-form-col{width:100%}.smb-commerce-settings-preview-col{position:relative}.preview-sticky{position:fixed;bottom:20px;right:20px;z-index:99999;width:350px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 10px 25px #00000026}.preview-sticky h3{margin-top:0;color:#1e293b;font-size:1.1rem}.widget-preview-box{background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 10px 20px #0000001a;display:flex;flex-direction:column;height:400px;border:1px solid #e2e8f0;margin-top:12px}.widget-preview-header{display:flex;justify-content:space-between;align-items:center;padding:16px;color:#fff}.widget-preview-header-brand{display:flex;align-items:center;gap:12px;font-weight:600;font-size:15px}.widget-preview-logo{width:32px;height:32px;border-radius:50%;object-fit:cover;background:#fff3}.widget-preview-close{font-size:24px;line-height:1;opacity:.8}.widget-preview-body{flex:1;background:#f8fafc;padding:16px;display:flex;flex-direction:column;gap:12px}.preview-message{padding:12px 16px;border-radius:16px;font-size:14px;line-height:1.4;max-width:85%}.preview-message.bot{background:#fff;align-self:flex-start;border-bottom-left-radius:4px;box-shadow:0 1px 2px #0000000d;color:#334155}.preview-message.user{align-self:flex-end;border-bottom-right-radius:4px;box-shadow:0 1px 2px #0000001a}.widget-preview-footer{padding:12px;background:#fff;border-top:1px solid #e2e8f0;display:flex;gap:8px}.widget-preview-footer input{flex:1;border:1px solid #e2e8f0;border-radius:20px;padding:8px 16px;background:#f8fafc;font-size:14px;outline:none}.widget-preview-footer button{width:36px;height:36px;border-radius:50%;border:none;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}.smb-commerce-card{background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 3px #0000001a,0 1px 2px #0000000f;padding:20px;margin-bottom:20px;border-radius:8px;transition:box-shadow .2s}.smb-commerce-card:hover{box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.smb-commerce-card h3{margin-top:0;padding-bottom:12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;color:#0f172a;font-size:1.1rem}.smb-commerce-form-group{margin-bottom:16px}.smb-commerce-form-group-row{display:flex;gap:20px}.smb-commerce-form-group-row>div{flex:1}.smb-commerce-form-group label{display:block;font-weight:600;margin-bottom:8px;color:#334155}.smb-commerce-form-group input[type=text],.smb-commerce-form-group input[type=password],.smb-commerce-form-group select{width:100%;max-width:none;padding:10px 12px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;box-shadow:0 1px 2px #0000000d inset;transition:border-color .2s,box-shadow .2s}.smb-commerce-form-group input[type=text]:focus,.smb-commerce-form-group input[type=password]:focus,.smb-commerce-form-group select:focus{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a;outline:none}.color-picker-wrapper{display:flex;align-items:center;gap:12px}.color-picker-wrapper input[type=color]{width:48px;height:48px;padding:0;border:2px solid #e2e8f0;border-radius:8px;cursor:pointer;background:none}.color-picker-wrapper .color-hex{font-family:monospace;background:#f1f5f9;padding:6px 12px;border-radius:4px;border:1px solid #e2e8f0}.toggle-group{display:flex;gap:16px;align-items:flex-start}.toggle-switch{position:relative;display:inline-block;width:50px;height:28px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5e1;transition:.4s;border-radius:34px}.toggle-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%;box-shadow:0 2px 4px #0003}input:checked+.toggle-slider{background-color:#01696f}input:checked+.toggle-slider:before{transform:translate(22px)}.smb-commerce-notice{background:#fff;border-left:4px solid #00a32a;box-shadow:0 1px 1px #0000000a;padding:1px 12px;margin-bottom:20px}.smb-commerce-inbox-container{display:flex;height:70vh;background:#fff;border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px #0000000a;overflow:hidden}.smb-commerce-sidebar{width:300px;border-right:1px solid #ccd0d4;overflow-y:auto;background:#f9f9f9}.smb-commerce-conversation-item{padding:15px;border-bottom:1px solid #e2e4e7;cursor:pointer;transition:background .2s}.smb-commerce-conversation-item:hover,.smb-commerce-conversation-item.active{background:#fff;border-left:3px solid #2271b1}.smb-commerce-main{flex:1;display:flex;flex-direction:column}.smb-commerce-main-header{padding:15px;border-bottom:1px solid #ccd0d4;display:flex;justify-content:space-between;align-items:center;background:#fff}.smb-commerce-chat-history{flex:1;padding:20px;overflow-y:auto;background:#f0f0f1;display:flex;flex-direction:column;gap:10px}.smb-commerce-admin-msg{max-width:70%;padding:10px 14px;border-radius:12px}.smb-commerce-admin-msg.user{align-self:flex-start;background:#fff;border:1px solid #ddd}.smb-commerce-admin-msg.agent,.smb-commerce-admin-msg.bot{align-self:flex-end;background:#d4edda;border:1px solid #c3e6cb}.smb-commerce-main-input{padding:15px;background:#fff;border-top:1px solid #ccd0d4;display:flex;gap:10px}.smb-commerce-main-input input{flex:1;padding:8px 12px}.smb-commerce-admin-wrapper,.smb-commerce-admin-wrapper *,.smb-commerce-admin-wrapper *:before,.smb-commerce-admin-wrapper *:after,.smb-commerce-settings-wrapper,.smb-commerce-settings-wrapper *,.smb-commerce-settings-wrapper *:before,.smb-commerce-settings-wrapper *:after{box-sizing:border-box!important}.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{min-width:0}.smb-commerce-admin-wrapper td,.smb-commerce-admin-wrapper th,.smb-commerce-settings-wrapper td,.smb-commerce-settings-wrapper th,.smb-commerce-admin-msg{overflow-wrap:anywhere!important;word-break:break-word!important}.smb-notices .smb-inline-notice,.smb-inline-notice{position:relative;margin:0 0 12px}.smb-confirm{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:0 0 16px;padding:12px 16px;background:#fcf0f1;border-left:4px solid #d63638;box-shadow:0 1px 1px #0000000a}.smb-confirm p{margin:0;font-weight:600;color:#1d2327}.smb-confirm-actions{display:flex;gap:8px}.smb-commerce-admin-wrapper .button.smb-button-danger,.smb-commerce-settings-wrapper .button.smb-button-danger{background:#b32d2e;border-color:#b32d2e;color:#fff}.smb-field-error{margin:4px 0 0;color:#b32d2e}.smb-value-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:16px}.smb-value-header h2{margin:0 0 4px;font-size:1.4rem}.smb-value-intro{margin:0;max-width:70ch;color:#50575e}.smb-value-meta,.smb-value-status{margin:6px 0 0;color:#50575e}.smb-value-period{display:flex;flex-direction:column;gap:4px;font-weight:600}.smb-value-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}.smb-value-card{background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:20px}.smb-value-card h3{margin:0;font-size:1.05rem;color:#1d2327}.smb-value-card h4{margin:18px 0 6px;font-size:.95rem}.smb-value-sample{margin:2px 0 12px;font-size:12px;color:#646970}.smb-value-rows{margin:0}.smb-value-rows>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:8px 0;border-top:1px solid #f0f0f1}.smb-value-rows dt{color:#3c434a}.smb-value-rows dd{margin:0;text-align:right}.smb-value-total dt,.smb-value-total dd{font-weight:600}.smb-value-sub{display:block;margin-top:2px;font-size:12px;color:#646970}.smb-value-sub-inline{font-size:12px;color:#646970}.smb-value-big{margin:4px 0;font-size:14px}.smb-value-big strong{font-size:2rem;line-height:1.1;margin-right:4px}.smb-value-caption{margin:4px 0 0;font-weight:600;color:#3c434a}.smb-value-note,.smb-value-empty{margin:10px 0 0;font-size:12px;color:#50575e}.smb-value-empty{font-size:13px}.smb-value-list{margin:0;padding:0;list-style:none}.smb-value-sources li{display:flex;justify-content:space-between;padding:4px 0;border-top:1px solid #f0f0f1}.smb-value-settings{margin-top:20px;background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:0 20px}.smb-value-settings summary{padding:14px 0;font-weight:600;cursor:pointer}.smb-value-settings form{padding-bottom:20px}.smb-value-settings fieldset{margin:0 0 16px;padding:0;border:0}.smb-value-settings legend{margin-bottom:6px;font-weight:600}.smb-value-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px 16px}.smb-value-fields>label{display:flex;flex-direction:column;gap:4px}.smb-value-group-label{display:block;margin-bottom:4px}.smb-value-check{display:flex;align-items:center;gap:6px;min-height:24px}.smb-value-settings summary:focus-visible,.smb-value-period select:focus-visible{outline:2px solid #2271b1;outline-offset:2px}
     1.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{margin:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.smb-commerce-settings-layout{display:grid;grid-template-columns:1fr 380px;gap:20px;align-items:start}@media (max-width: 1024px){.smb-commerce-settings-layout{grid-template-columns:1fr}}.smb-commerce-settings-form-col{width:100%}.smb-commerce-settings-preview-col{position:relative}.preview-sticky{position:fixed;bottom:20px;right:20px;z-index:99999;width:350px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 10px 25px #00000026}.preview-sticky h3{margin-top:0;color:#1e293b;font-size:1.1rem}.widget-preview-box{background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 10px 20px #0000001a;display:flex;flex-direction:column;height:400px;border:1px solid #e2e8f0;margin-top:12px}.widget-preview-header{display:flex;justify-content:space-between;align-items:center;padding:16px;color:#fff}.widget-preview-header-brand{display:flex;align-items:center;gap:12px;font-weight:600;font-size:15px}.widget-preview-logo{width:32px;height:32px;border-radius:50%;object-fit:cover;background:#fff3}.widget-preview-close{font-size:24px;line-height:1;opacity:.8}.widget-preview-body{flex:1;background:#f8fafc;padding:16px;display:flex;flex-direction:column;gap:12px}.preview-message{padding:12px 16px;border-radius:16px;font-size:14px;line-height:1.4;max-width:85%}.preview-message.bot{background:#fff;align-self:flex-start;border-bottom-left-radius:4px;box-shadow:0 1px 2px #0000000d;color:#334155}.preview-message.user{align-self:flex-end;border-bottom-right-radius:4px;box-shadow:0 1px 2px #0000001a}.widget-preview-footer{padding:12px;background:#fff;border-top:1px solid #e2e8f0;display:flex;gap:8px}.widget-preview-footer input{flex:1;border:1px solid #e2e8f0;border-radius:20px;padding:8px 16px;background:#f8fafc;font-size:14px;outline:none}.widget-preview-footer button{width:36px;height:36px;border-radius:50%;border:none;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}.smb-commerce-card{background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 3px #0000001a,0 1px 2px #0000000f;padding:20px;margin-bottom:20px;border-radius:8px;transition:box-shadow .2s}.smb-commerce-card:hover{box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.smb-commerce-card h3{margin-top:0;padding-bottom:12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;color:#0f172a;font-size:1.1rem}.smb-commerce-form-group{margin-bottom:16px}.smb-commerce-form-group-row{display:flex;gap:20px}.smb-commerce-form-group-row>div{flex:1}.smb-commerce-form-group label{display:block;font-weight:600;margin-bottom:8px;color:#334155}.smb-commerce-form-group input[type=text],.smb-commerce-form-group input[type=password],.smb-commerce-form-group select{width:100%;max-width:none;padding:10px 12px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;box-shadow:0 1px 2px #0000000d inset;transition:border-color .2s,box-shadow .2s}.smb-commerce-form-group input[type=text]:focus,.smb-commerce-form-group input[type=password]:focus,.smb-commerce-form-group select:focus{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a;outline:none}.color-picker-wrapper{display:flex;align-items:center;gap:12px}.color-picker-wrapper input[type=color]{width:48px;height:48px;padding:0;border:2px solid #e2e8f0;border-radius:8px;cursor:pointer;background:none}.color-picker-wrapper .color-hex{font-family:monospace;background:#f1f5f9;padding:6px 12px;border-radius:4px;border:1px solid #e2e8f0}.toggle-group{display:flex;gap:16px;align-items:flex-start}.toggle-switch{position:relative;display:inline-block;width:50px;height:28px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5e1;transition:.4s;border-radius:34px}.toggle-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%;box-shadow:0 2px 4px #0003}input:checked+.toggle-slider{background-color:#01696f}input:checked+.toggle-slider:before{transform:translate(22px)}.smb-commerce-notice{background:#fff;border-left:4px solid #00a32a;box-shadow:0 1px 1px #0000000a;padding:1px 12px;margin-bottom:20px}.smb-commerce-inbox-container{display:flex;height:70vh;background:#fff;border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px #0000000a;overflow:hidden}.smb-commerce-sidebar{width:300px;border-right:1px solid #ccd0d4;overflow-y:auto;background:#f9f9f9}.smb-commerce-conversation-item{padding:15px;border-bottom:1px solid #e2e4e7;cursor:pointer;transition:background .2s}.smb-commerce-conversation-item:hover,.smb-commerce-conversation-item.active{background:#fff;border-left:3px solid #2271b1}.smb-commerce-main{flex:1;display:flex;flex-direction:column}.smb-commerce-main-header{padding:15px;border-bottom:1px solid #ccd0d4;display:flex;justify-content:space-between;align-items:center;background:#fff}.smb-commerce-chat-history{flex:1;padding:20px;overflow-y:auto;background:#f0f0f1;display:flex;flex-direction:column;gap:10px}.smb-commerce-admin-msg{max-width:70%;padding:10px 14px;border-radius:12px}.smb-commerce-admin-msg.user{align-self:flex-start;background:#fff;border:1px solid #ddd}.smb-commerce-admin-msg.agent,.smb-commerce-admin-msg.bot{align-self:flex-end;background:#d4edda;border:1px solid #c3e6cb}.smb-customer-aside{width:260px;flex-shrink:0;border-left:1px solid #ccd0d4;overflow-y:auto;background:#fff;padding:14px}.smb-customer-inline{display:none;border-bottom:1px solid #ccd0d4;padding:8px 15px;background:#f6f7f7}.smb-customer-inline summary{cursor:pointer;font-weight:600}.smb-customer-card h4{margin:0 0 8px;font-size:14px}.smb-customer-card dl{margin:0}.smb-customer-card dt{margin-top:10px;color:#646970;font-size:12px}.smb-customer-card dd{margin:2px 0 0;overflow-wrap:anywhere}.smb-customer-card ul{margin:0;padding-left:16px;list-style:disc}.smb-customer-sub,.smb-customer-note{color:#646970;font-size:12px}.smb-customer-links{margin:12px 0 0}.smb-customer-warn{margin:0 0 8px;padding:6px 8px;background:#fcf9e8;border-left:3px solid #dba617;font-size:12px}@media (max-width: 1100px){.smb-customer-aside{display:none}.smb-customer-inline{display:block}}.smb-commerce-main-input{padding:15px;background:#fff;border-top:1px solid #ccd0d4;display:flex;gap:10px}.smb-commerce-main-input input{flex:1;padding:8px 12px}.smb-commerce-admin-wrapper,.smb-commerce-admin-wrapper *,.smb-commerce-admin-wrapper *:before,.smb-commerce-admin-wrapper *:after,.smb-commerce-settings-wrapper,.smb-commerce-settings-wrapper *,.smb-commerce-settings-wrapper *:before,.smb-commerce-settings-wrapper *:after{box-sizing:border-box!important}.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{min-width:0}.smb-commerce-admin-wrapper td,.smb-commerce-admin-wrapper th,.smb-commerce-settings-wrapper td,.smb-commerce-settings-wrapper th,.smb-commerce-admin-msg{overflow-wrap:anywhere!important;word-break:break-word!important}.smb-notices .smb-inline-notice,.smb-inline-notice{position:relative;margin:0 0 12px}.smb-confirm{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:0 0 16px;padding:12px 16px;background:#fcf0f1;border-left:4px solid #d63638;box-shadow:0 1px 1px #0000000a}.smb-confirm p{margin:0;font-weight:600;color:#1d2327}.smb-confirm-actions{display:flex;gap:8px}.smb-commerce-admin-wrapper .button.smb-button-danger,.smb-commerce-settings-wrapper .button.smb-button-danger{background:#b32d2e;border-color:#b32d2e;color:#fff}.smb-field-error{margin:4px 0 0;color:#b32d2e}.smb-value-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:16px}.smb-value-header h2{margin:0 0 4px;font-size:1.4rem}.smb-value-intro{margin:0;max-width:70ch;color:#50575e}.smb-value-meta,.smb-value-status{margin:6px 0 0;color:#50575e}.smb-value-period{display:flex;flex-direction:column;gap:4px;font-weight:600}.smb-value-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}.smb-value-card{background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:20px}.smb-value-card h3{margin:0;font-size:1.05rem;color:#1d2327}.smb-value-card h4{margin:18px 0 6px;font-size:.95rem}.smb-value-sample{margin:2px 0 12px;font-size:12px;color:#646970}.smb-value-rows{margin:0}.smb-value-rows>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:8px 0;border-top:1px solid #f0f0f1}.smb-value-rows dt{color:#3c434a}.smb-value-rows dd{margin:0;text-align:right}.smb-value-total dt,.smb-value-total dd{font-weight:600}.smb-value-sub{display:block;margin-top:2px;font-size:12px;color:#646970}.smb-value-sub-inline{font-size:12px;color:#646970}.smb-value-big{margin:4px 0;font-size:14px}.smb-value-big strong{font-size:2rem;line-height:1.1;margin-right:4px}.smb-value-caption{margin:4px 0 0;font-weight:600;color:#3c434a}.smb-value-note,.smb-value-empty{margin:10px 0 0;font-size:12px;color:#50575e}.smb-value-empty{font-size:13px}.smb-value-list{margin:0;padding:0;list-style:none}.smb-value-sources li{display:flex;justify-content:space-between;padding:4px 0;border-top:1px solid #f0f0f1}.smb-value-settings{margin-top:20px;background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:0 20px}.smb-value-settings summary{padding:14px 0;font-weight:600;cursor:pointer}.smb-value-settings form{padding-bottom:20px}.smb-value-settings fieldset{margin:0 0 16px;padding:0;border:0}.smb-value-settings legend{margin-bottom:6px;font-weight:600}.smb-value-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px 16px}.smb-value-fields>label{display:flex;flex-direction:column;gap:4px}.smb-value-group-label{display:block;margin-bottom:4px}.smb-value-check{display:flex;align-items:center;gap:6px;min-height:24px}.smb-value-settings summary:focus-visible,.smb-value-period select:focus-visible{outline:2px solid #2271b1;outline-offset:2px}.smb-insights-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;align-items:start}.smb-insights-wide{grid-column:1 / -1}.smb-insights-table td{vertical-align:top}.smb-insights-table td:nth-child(2),.smb-insights-table td:nth-child(3){white-space:nowrap}.smb-insights-table td strong{overflow-wrap:anywhere}.smb-insights-actions{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:flex-end}.smb-insights-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.smb-insights-tag{padding:1px 8px;border-radius:999px;background:#f0f0f1;color:#50575e;font-size:11px}.smb-insights-draft form{display:grid;gap:8px;max-width:720px}.smb-insights-draft label{display:grid;gap:4px;font-weight:600}.smb-insights-draft fieldset{margin:0;padding:0;border:0}.smb-insights-draft fieldset label{display:flex;font-weight:400}.smb-insights-notes ul{margin:0}.smb-insights-notes li{padding:6px 0;border-top:1px solid #f0f0f1}.smb-insights-list li{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-top:1px solid #f0f0f1}.smb-insights-list li>span:first-child{min-width:0;overflow-wrap:anywhere}.smb-insights-list li>span:last-child{flex-shrink:0;text-align:right}.smb-insights-list a,.smb-insights-list .button-link,.smb-insights-actions .button-link{white-space:nowrap}.smb-insights-why{margin:0}.smb-insights-why li{padding:8px 0;border-top:1px solid #f0f0f1}.smb-insights-why-row{display:flex;justify-content:space-between;gap:12px}.smb-insights-bar{height:6px;margin-top:4px;border-radius:999px;background:#f0f0f1;overflow:hidden}.smb-insights-bar span{display:block;height:100%;background:#2271b1}.smb-insights-quote{margin:4px 0 0;padding-left:8px;border-left:3px solid #dcdcde;color:#50575e;font-size:12px;overflow-wrap:anywhere}@media (max-width: 782px){.smb-insights-table thead{display:none}.smb-insights-table tr,.smb-insights-table td{display:block;width:auto}.smb-insights-table td[data-label]:before{content:attr(data-label) ": ";color:#646970}.smb-insights-table td.smb-insights-actions{display:flex;justify-content:flex-start}.smb-insights-list li>span:last-child{flex-shrink:1}}
  • ninhle-ai-sales-assistant/tags/2.6.0/assets/dist/manifest.json

    r3761175 r3765552  
    11{
    2   "_hooks.module-C33f2O-M.js": {
    3     "file": "assets/hooks.module-C33f2O-M.js",
    4     "name": "hooks.module"
    5   },
    6   "_widget-shared-BrY9zvIQ.js": {
    7     "file": "assets/widget-shared-BrY9zvIQ.js",
    8     "name": "widget-shared"
    9   },
    10   "src/JS/admin/index.tsx": {
    11     "file": "admin.bundle.js",
    12     "name": "admin",
    13     "src": "src/JS/admin/index.tsx",
    14     "isEntry": true,
    15     "imports": [
    16       "_hooks.module-C33f2O-M.js"
    17     ],
    18     "css": [
    19       "admin.css"
    20     ]
    21   },
    22   "src/JS/widget/components/ChatWindow.tsx": {
    23     "file": "assets/ChatWindow-BtSh_sym.js",
    24     "name": "ChatWindow",
    25     "src": "src/JS/widget/components/ChatWindow.tsx",
    26     "isDynamicEntry": true,
    27     "imports": [
    28       "_widget-shared-BrY9zvIQ.js",
    29       "_hooks.module-C33f2O-M.js"
    30     ],
    31     "dynamicImports": [
    32       "src/JS/widget/voice.ts"
    33     ]
    34   },
    35   "src/JS/widget/index.tsx": {
    36     "file": "widget.bundle.js",
    37     "name": "widget",
    38     "src": "src/JS/widget/index.tsx",
    39     "isEntry": true,
    40     "imports": [
    41       "_widget-shared-BrY9zvIQ.js",
    42       "_hooks.module-C33f2O-M.js"
    43     ],
    44     "dynamicImports": [
    45       "src/JS/widget/components/ChatWindow.tsx"
    46     ],
    47     "css": [
    48       "widget.css"
    49     ]
    50   },
    51   "src/JS/widget/voice.ts": {
    52     "file": "assets/voice-DtEIuG21.js",
    53     "name": "voice",
    54     "src": "src/JS/widget/voice.ts",
    55     "isDynamicEntry": true
    56   }
     2    "_ChatWindow-D3sL_SAs.js":  {
     3                                    "file":  "assets/ChatWindow-D3sL_SAs.js",
     4                                    "name":  "ChatWindow",
     5                                    "isDynamicEntry":  true,
     6                                    "imports":  [
     7                                                    "_widget-shared-CHYtJpo8.js",
     8                                                    "_hooks.module-C33f2O-M.js"
     9                                                ],
     10                                    "dynamicImports":  [
     11                                                           "src/JS/widget/voice.ts",
     12                                                           "src/JS/widget/components/ProductCards.tsx",
     13                                                           "src/JS/widget/components/CartPanel.tsx"
     14                                                       ]
     15                                },
     16    "_hooks.module-C33f2O-M.js":  {
     17                                      "file":  "assets/hooks.module-C33f2O-M.js",
     18                                      "name":  "hooks.module"
     19                                  },
     20    "_i18n-D_zElFbw.js":  {
     21                              "file":  "assets/i18n-D_zElFbw.js",
     22                              "name":  "i18n",
     23                              "imports":  [
     24                                              "_hooks.module-C33f2O-M.js"
     25                                          ]
     26                          },
     27    "_pro-CgluFZP2.js":  {
     28                             "file":  "assets/pro-CgluFZP2.js",
     29                             "name":  "pro",
     30                             "imports":  [
     31                                             "_hooks.module-C33f2O-M.js"
     32                                         ]
     33                         },
     34    "_widget-shared-CHYtJpo8.js":  {
     35                                       "file":  "assets/widget-shared-CHYtJpo8.js",
     36                                       "name":  "widget-shared"
     37                                   },
     38    "src/JS/admin/index.tsx":  {
     39                                   "file":  "admin.bundle.js",
     40                                   "name":  "admin",
     41                                   "src":  "src/JS/admin/index.tsx",
     42                                   "isEntry":  true,
     43                                   "imports":  [
     44                                                   "_hooks.module-C33f2O-M.js",
     45                                                   "_i18n-D_zElFbw.js"
     46                                               ],
     47                                   "css":  [
     48                                               "admin.css"
     49                                           ]
     50                               },
     51    "src/JS/admin/insights/index.tsx":  {
     52                                            "file":  "admin-insights.bundle.js",
     53                                            "name":  "admin-insights",
     54                                            "src":  "src/JS/admin/insights/index.tsx",
     55                                            "isEntry":  true,
     56                                            "imports":  [
     57                                                            "_hooks.module-C33f2O-M.js",
     58                                                            "_i18n-D_zElFbw.js"
     59                                                        ]
     60                                        },
     61    "src/JS/widget/cart.ts":  {
     62                                  "file":  "assets/cart-BtM4a74e.js",
     63                                  "name":  "cart",
     64                                  "src":  "src/JS/widget/cart.ts",
     65                                  "isDynamicEntry":  true
     66                              },
     67    "src/JS/widget/components/CartPanel.tsx":  {
     68                                                   "file":  "assets/CartPanel-D6-1EaTY.js",
     69                                                   "name":  "CartPanel",
     70                                                   "src":  "src/JS/widget/components/CartPanel.tsx",
     71                                                   "isDynamicEntry":  true,
     72                                                   "imports":  [
     73                                                                   "_hooks.module-C33f2O-M.js",
     74                                                                   "_widget-shared-CHYtJpo8.js",
     75                                                                   "src/JS/widget/cart.ts",
     76                                                                   "_pro-CgluFZP2.js"
     77                                                               ]
     78                                               },
     79    "src/JS/widget/components/ProductCards.tsx":  {
     80                                                      "file":  "assets/ProductCards-81V8biLP.js",
     81                                                      "name":  "ProductCards",
     82                                                      "src":  "src/JS/widget/components/ProductCards.tsx",
     83                                                      "isDynamicEntry":  true,
     84                                                      "imports":  [
     85                                                                      "_widget-shared-CHYtJpo8.js",
     86                                                                      "_hooks.module-C33f2O-M.js",
     87                                                                      "_ChatWindow-D3sL_SAs.js",
     88                                                                      "_pro-CgluFZP2.js"
     89                                                                  ],
     90                                                      "dynamicImports":  [
     91                                                                             "src/JS/widget/cart.ts"
     92                                                                         ]
     93                                                  },
     94    "src/JS/widget/index.tsx":  {
     95                                    "file":  "widget.bundle.js",
     96                                    "name":  "widget",
     97                                    "src":  "src/JS/widget/index.tsx",
     98                                    "isEntry":  true,
     99                                    "imports":  [
     100                                                    "_widget-shared-CHYtJpo8.js",
     101                                                    "_hooks.module-C33f2O-M.js"
     102                                                ],
     103                                    "dynamicImports":  [
     104                                                           "_ChatWindow-D3sL_SAs.js"
     105                                                       ],
     106                                    "css":  [
     107                                                "widget.css"
     108                                            ]
     109                                },
     110    "src/JS/widget/voice.ts":  {
     111                                   "file":  "assets/voice-DkonJhm8.js",
     112                                   "name":  "voice",
     113                                   "src":  "src/JS/widget/voice.ts",
     114                                   "isDynamicEntry":  true
     115                               }
    57116}
  • ninhle-ai-sales-assistant/tags/2.6.0/assets/dist/widget.bundle.js

    r3761175 r3765552  
    1 const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./assets/ChatWindow-BtSh_sym.js","./assets/widget-shared-BrY9zvIQ.js","./assets/hooks.module-C33f2O-M.js"])))=>i.map(i=>d[i]);
    2 import{t as G,_ as J,a as D}from"./assets/widget-shared-BrY9zvIQ.js";import{u,R as Q,d as l,A as C,h as y}from"./assets/hooks.module-C33f2O-M.js";function X({onClick:s,onIntent:a,config:o,buttonRef:n}){return u("button",{type:"button",ref:n,className:`smb-launcher ${o!=null&&o.logoUrl?"has-logo":""}`,onClick:s,onPointerEnter:a,onTouchStart:a,onFocus:a,"aria-label":G("openChat"),"aria-haspopup":"dialog",children:o!=null&&o.logoUrl?u("img",{src:o.logoUrl,alt:""}):u("svg",{width:"26",height:"26",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:u("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})})})}function Y(s){const a=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s.trim());if(!a)return null;let o=a[1];o.length===3&&(o=o.split("").map(d=>d+d).join(""));const[n,_,h]=[0,2,4].map(d=>{const p=parseInt(o.slice(d,d+2),16)/255;return p<=.03928?p/12.92:Math.pow((p+.055)/1.055,2.4)});return .2126*n+.7152*_+.0722*h}const Z="#111111",ee=.0056;function te(s){const a=Y(s);if(a===null)return"#ffffff";const o=1.05/(a+.05),n=(a+.05)/(ee+.05);return o>=n?"#ffffff":Z}let E=null;function H(){return E||(E=J(()=>import("./assets/ChatWindow-BtSh_sym.js"),__vite__mapDeps([0,1,2]),import.meta.url).catch(s=>{throw E=null,s})),E}const ne=()=>{var T;const[s,a]=l(!1),o=C(!1),[n,_]=l(((T=window.SMBCommerceWidget)==null?void 0:T.widgetConfig)||null),[h,d]=l(null),[p,A]=l([]),[j,I]=l("bot_active"),[q,M]=l(null),k=C(null),L=C(null),N=C(!1),[$,W]=l(null),z=()=>{H().then(W).catch(()=>{})},F=document.body.classList,R=F.contains("woocommerce-checkout")||F.contains("woocommerce-cart");y(()=>{const e=window.visualViewport,t=k.current;if(!e||!t)return;let i=0;const m=()=>{if(i=0,!(window.innerWidth>document.documentElement.clientWidth+1)||t.classList.contains("is-open")){t.style.left=t.style.top=t.style.right=t.style.bottom="";return}const f=12,b=t.classList.contains("is-commerce-page")&&e.width<768?96:f;t.style.right="auto",t.style.bottom="auto",t.style.left=Math.max(e.offsetLeft+f,e.offsetLeft+e.width-t.offsetWidth-f)+"px",t.style.top=Math.max(e.offsetTop+f,e.offsetTop+e.height-t.offsetHeight-b)+"px"},c=()=>{i||(i=requestAnimationFrame(m))};e.addEventListener("resize",c),e.addEventListener("scroll",c),window.addEventListener("resize",c);const r=typeof ResizeObserver<"u"?new ResizeObserver(c):null;return r==null||r.observe(t),r==null||r.observe(document.documentElement),c(),()=>{e.removeEventListener("resize",c),e.removeEventListener("scroll",c),window.removeEventListener("resize",c),r==null||r.disconnect(),i&&cancelAnimationFrame(i)}},[]);const S=async()=>{if(h!==null)return!0;if(o.current)return!1;o.current=!0;try{const e=await D.bootstrap();return _(t=>({...t||{},...e.widget_config||{}})),d(e.conversation_id),A(e.messages||[]),I(e.status||"bot_active"),M(e.agent_name||null),!0}catch(e){return console.error("Failed to initialize Ninhle AI Sales Assistant widget",e),!1}finally{o.current=!1}};y(()=>{D.hasSession()&&(S(),z())},[]);const[x,B]=l(!1);y(()=>{if(!R||s){B(!1);return}if(typeof IntersectionObserver>"u")return;const e="#place_order, .wc-block-components-checkout-place-order-button, .checkout-button, .wc-block-cart__submit-button";let t=null;const i=new Map,m=()=>{var b;t==null||t.disconnect(),i.clear();const r=(b=L.current)==null?void 0:b.getBoundingClientRect();if(!r)return;const g=v=>`${-Math.max(0,Math.round(v))}px`,f=document.documentElement.clientWidth;t=new IntersectionObserver(v=>{v.forEach(U=>i.set(U.target,U.isIntersecting)),B(Array.from(i.values()).some(Boolean))},{rootMargin:`${g(r.top-8)} ${g(f-r.right)} ${g(window.innerHeight-r.bottom-8)} ${g(r.left)}`}),document.querySelectorAll(e).forEach(v=>t.observe(v))};m(),window.addEventListener("resize",m);const c=window.setInterval(m,2e3);return()=>{t==null||t.disconnect(),window.removeEventListener("resize",m),clearInterval(c)}},[s]),y(()=>{var e;!s&&N.current&&((e=L.current)==null||e.focus()),N.current=s},[s]);const K=async()=>{let e;try{e=await H()}catch(t){console.error("Failed to load the Ninhle AI Sales Assistant chat window",t);return}W(e),await S()&&a(!0)},w=n!=null&&n.fontFamily&&n.fontFamily!=="system"?n.fontFamily:"";y(()=>{if(w){const e=w,t=`smb-commerce-font-${e.replace(/\s+/g,"-")}`;if(!document.getElementById(t)){const i=document.createElement("link");i.id=t,i.rel="stylesheet",i.href=`https://fonts.googleapis.com/css2?family=${e.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(i)}}},[w]);const O=(n==null?void 0:n.primaryColor)||(n==null?void 0:n.primary_color)||"#01696f",P={"--smb-primary":O,"--smb-on-primary":te(O)};return w&&(P.fontFamily=`'${w}', sans-serif`),u("div",{ref:k,className:`smb-commerce-widget-container ${s?"is-open":""} ${R?"is-commerce-page":""} ${x?"is-dodging":""}`,style:P,"aria-hidden":x?"true":void 0,children:s&&h!==null&&$?u($.ChatWindow,{onClose:()=>a(!1),config:n,conversationId:h,initialMessages:p,initialStatus:j,initialAgentName:q,onSnapshot:e=>{A(e.messages),I(e.status),M(e.agentName)}}):u(X,{onClick:K,onIntent:z,config:n,buttonRef:L})})},V=document.getElementById("smb-commerce-widget-root");V&&Q(u(ne,{}),V);
     1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./assets/ChatWindow-D3sL_SAs.js","./assets/widget-shared-CHYtJpo8.js","./assets/hooks.module-C33f2O-M.js"])))=>i.map(i=>d[i]);
     2import{t as O,_ as re,a as R}from"./assets/widget-shared-CHYtJpo8.js";import{u as w,R as se,d as h,A as b,h as g}from"./assets/hooks.module-C33f2O-M.js";function oe({onClick:e,onIntent:r,config:s,buttonRef:o,unread:d=0}){return w("button",{type:"button",ref:o,className:`smb-launcher ${s!=null&&s.logoUrl?"has-logo":""}`,onClick:e,onPointerEnter:r,onTouchStart:r,onFocus:r,"aria-label":d>0?`${O("openChat")} (${O("unreadMessages").replace("%d",String(d))})`:O("openChat"),"aria-haspopup":"dialog",children:[s!=null&&s.logoUrl?w("img",{src:s.logoUrl,alt:""}):w("svg",{width:"26",height:"26",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:w("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),d>0&&w("span",{className:"smb-unread","aria-hidden":"true",children:d>9?"9+":d})]})}function ie(e){const r=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e.trim());if(!r)return null;let s=r[1];s.length===3&&(s=s.split("").map(p=>p+p).join(""));const[o,d,c]=[0,2,4].map(p=>{const m=parseInt(s.slice(p,p+2),16)/255;return m<=.03928?m/12.92:Math.pow((m+.055)/1.055,2.4)});return .2126*o+.7152*d+.0722*c}const ae="#111111",ce=.0056;function le(e){const r=ie(e);if(r===null)return"#ffffff";const s=1.05/(r+.05),o=(r+.05)/(ce+.05);return s>=o?"#ffffff":ae}const ue=25e3,de=30*6e4;function me(e){return e.open||!e.conversationId||!e.visible?!1:e.status==="agent_active"||e.status==="human_active"?!0:e.lastActivityAt>0&&e.now-e.lastActivityAt<de}function G(e,r){return e.filter(s=>s.sender_type!=="user"&&Number(s.id)>r).length}function C(e){return e.reduce((r,s)=>Math.max(r,Number(s.id)||0),0)}const Z="smbcommerce_seen";function fe(e){try{const r=JSON.parse(localStorage.getItem(Z)||"null");if(r&&Number(r.c)===Number(e))return{id:Number(r.id)||0,at:Number(r.at)||0}}catch{}return{id:0,at:0}}function x(e,r,s){try{localStorage.setItem(Z,JSON.stringify({c:Number(e),id:r,at:s}))}catch{}}function he(e,r){if(!r.length)return e;const s=new Set(r.map(o=>o.id));return[...e.filter(o=>!s.has(o.id)),...r].sort((o,d)=>Number(o.id)-Number(d.id))}let A=null;function Q(){return A||(A=re(()=>import("./assets/ChatWindow-D3sL_SAs.js").then(e=>e.C),__vite__mapDeps([0,1,2]),import.meta.url).catch(e=>{throw A=null,e})),A}const we=()=>{var J;const[e,r]=h(!1),s=b(!1),[o,d]=h(((J=window.SMBCommerceWidget)==null?void 0:J.widgetConfig)||null),[c,p]=h(null),[m,S]=h([]),[M,L]=h("bot_active"),[ee,z]=h(null),F=b(null),$=b(null),k=b(!1),[B,D]=h(null),[te,W]=h(0),l=b({id:0,at:0}),_=b({isOpen:e,conversationId:c,status:M,messages:m});_.current={isOpen:e,conversationId:c,status:M,messages:m},g(()=>{if(c===null)return;const t=fe(c);l.current=t.id>0?t:{id:C(m),at:Date.now()},t.id===0&&x(c,l.current.id,l.current.at),W(G(m,l.current.id))},[c]),g(()=>{c!==null&&(e||k.current)&&(l.current={id:C(m),at:Date.now()},x(c,l.current.id,l.current.at),W(0))},[e,m]),g(()=>{const t=window.setInterval(async()=>{const n=_.current;if(me({open:n.isOpen,conversationId:n.conversationId,status:n.status,lastActivityAt:l.current.at,visible:document.visibilityState==="visible",now:Date.now()}))try{const i=await R.pollMessages(n.conversationId,C(n.messages)||null);i.status&&L(i.status);const f=(i.messages||[]).filter(a=>Number(a.id)>C(_.current.messages));if(!f.length)return;const u=he(_.current.messages,f);S(u),l.current={...l.current,at:Date.now()},x(n.conversationId,l.current.id,l.current.at),W(G(u,l.current.id))}catch{}},ue);return()=>window.clearInterval(t)},[]);const P=()=>{Q().then(D).catch(()=>{})},U=document.body.classList,T=U.contains("woocommerce-checkout")||U.contains("woocommerce-cart");g(()=>{const t=window.visualViewport,n=F.current;if(!t||!n)return;let i=0;const f=()=>{if(i=0,!(window.innerWidth>document.documentElement.clientWidth+1)||n.classList.contains("is-open")){n.style.left=n.style.top=n.style.right=n.style.bottom="";return}const v=12,I=n.classList.contains("is-commerce-page")&&t.width<768?96:v;n.style.right="auto",n.style.bottom="auto",n.style.left=Math.max(t.offsetLeft+v,t.offsetLeft+t.width-n.offsetWidth-v)+"px",n.style.top=Math.max(t.offsetTop+v,t.offsetTop+t.height-n.offsetHeight-I)+"px"},u=()=>{i||(i=requestAnimationFrame(f))};t.addEventListener("resize",u),t.addEventListener("scroll",u),window.addEventListener("resize",u);const a=typeof ResizeObserver<"u"?new ResizeObserver(u):null;return a==null||a.observe(n),a==null||a.observe(document.documentElement),u(),()=>{t.removeEventListener("resize",u),t.removeEventListener("scroll",u),window.removeEventListener("resize",u),a==null||a.disconnect(),i&&cancelAnimationFrame(i)}},[]);const K=async()=>{if(c!==null)return!0;if(s.current)return!1;s.current=!0;try{const t=await R.bootstrap();return d(n=>({...n||{},...t.widget_config||{}})),p(t.conversation_id),S(t.messages||[]),L(t.status||"bot_active"),z(t.agent_name||null),!0}catch(t){return console.error("Failed to initialize Ninhle AI Sales Assistant widget",t),!1}finally{s.current=!1}};g(()=>{R.hasSession()&&(K(),P())},[]);const[V,H]=h(!1);g(()=>{if(!T||e){H(!1);return}if(typeof IntersectionObserver>"u")return;const t="#place_order, .wc-block-components-checkout-place-order-button, .checkout-button, .wc-block-cart__submit-button";let n=null;const i=new Map,f=()=>{var I;n==null||n.disconnect(),i.clear();const a=(I=$.current)==null?void 0:I.getBoundingClientRect();if(!a)return;const E=N=>`${-Math.max(0,Math.round(N))}px`,v=document.documentElement.clientWidth;n=new IntersectionObserver(N=>{N.forEach(Y=>i.set(Y.target,Y.isIntersecting)),H(Array.from(i.values()).some(Boolean))},{rootMargin:`${E(a.top-8)} ${E(v-a.right)} ${E(window.innerHeight-a.bottom-8)} ${E(a.left)}`}),document.querySelectorAll(t).forEach(N=>n.observe(N))};f(),window.addEventListener("resize",f);const u=window.setInterval(f,2e3);return()=>{n==null||n.disconnect(),window.removeEventListener("resize",f),clearInterval(u)}},[e]),g(()=>{var t;!e&&k.current&&((t=$.current)==null||t.focus()),k.current=e},[e]);const ne=async()=>{let t;try{t=await Q()}catch(n){console.error("Failed to load the Ninhle AI Sales Assistant chat window",n);return}D(t),await K()&&r(!0)},y=o!=null&&o.fontFamily&&o.fontFamily!=="system"?o.fontFamily:"";g(()=>{if(y){const t=y,n=`smb-commerce-font-${t.replace(/\s+/g,"-")}`;if(!document.getElementById(n)){const i=document.createElement("link");i.id=n,i.rel="stylesheet",i.href=`https://fonts.googleapis.com/css2?family=${t.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(i)}}},[y]);const j=(o==null?void 0:o.primaryColor)||(o==null?void 0:o.primary_color)||"#01696f",q={"--smb-primary":j,"--smb-on-primary":le(j)};return y&&(q.fontFamily=`'${y}', sans-serif`),w("div",{ref:F,className:`smb-commerce-widget-container ${e?"is-open":""} ${T?"is-commerce-page":""} ${V?"is-dodging":""}`,style:q,"aria-hidden":V?"true":void 0,children:e&&c!==null&&B?w(B.ChatWindow,{onClose:()=>r(!1),config:o,conversationId:c,initialMessages:m,initialStatus:M,initialAgentName:ee,onSnapshot:t=>{S(t.messages),L(t.status),z(t.agentName)}}):w(oe,{onClick:ne,onIntent:P,config:o,buttonRef:$,unread:te})})},X=document.getElementById("smb-commerce-widget-root");X&&se(w(we,{}),X);
  • ninhle-ai-sales-assistant/tags/2.6.0/assets/dist/widget.css

    r3761175 r3765552  
    1 .smb-commerce-widget-container{--smb-bg: #ffffff;--smb-bg-soft: #f1f3f6;--smb-text: #1f2328;--smb-text-soft: #555b66;--smb-radius: 20px;--smb-shadow: 0 16px 40px rgba(15, 23, 42, .2), 0 2px 8px rgba(15, 23, 42, .08);--smb-focus: #2563eb;position:fixed;right:20px;bottom:20px;z-index:999999;min-width:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.45;color:var(--smb-text);text-align:left}.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{box-sizing:border-box!important}.smb-commerce-widget-container button,.smb-commerce-widget-container input,.smb-commerce-widget-container textarea{margin:0;font:inherit;letter-spacing:normal;text-transform:none}.smb-commerce-widget-container button,.smb-commerce-widget-container a{border:0;background:none;box-shadow:none;color:inherit;cursor:pointer;padding:0;min-height:0;text-decoration:none;line-height:1.2}.smb-commerce-widget-container button:disabled{cursor:default;opacity:.45}.smb-commerce-widget-container :focus-visible:not(textarea){outline:2px solid var(--smb-focus)!important;outline-offset:2px}.smb-commerce-widget-container h2,.smb-commerce-widget-container h3,.smb-commerce-widget-container p{margin:0;padding:0;font-family:inherit;letter-spacing:normal}.smb-commerce-widget-container .smb-launcher{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary);box-shadow:0 6px 20px #0f172a3d;transition:transform .18s ease}.smb-commerce-widget-container .smb-launcher:hover{transform:scale(1.06)}.smb-commerce-widget-container .smb-launcher.has-logo{overflow:hidden}.smb-launcher img{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%}.smb-chat{width:380px;height:min(620px,calc(100vh - 40px));display:flex;flex-direction:column;overflow:hidden;background:var(--smb-bg);border-radius:var(--smb-radius);box-shadow:var(--smb-shadow);animation:smb-in .18s ease-out}.smb-chat.is-expanded{width:min(720px,calc(100vw - 40px));height:min(840px,calc(100vh - 40px))}@keyframes smb-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.smb-chat-header{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 10px 12px 16px;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-chat-identity{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.smb-chat-logo{flex-shrink:0;width:36px;height:36px;border-radius:50%;object-fit:cover;background:#ffffff38}.smb-chat-titles{min-width:0}.smb-commerce-widget-container .smb-chat-title{font-size:15px;font-weight:600;line-height:1.25;color:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.smb-commerce-widget-container .smb-chat-status{display:flex;align-items:center;gap:6px;font-size:12px;color:inherit;opacity:.92}.smb-status-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;background:#22c55e}.smb-status-dot.is-human_active{background:#f59e0b}.smb-status-dot.is-agent_active{background:#60a5fa}.smb-status-dot.is-closed{background:#9ca3af}.smb-chat-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.smb-commerce-widget-container .smb-icon-btn{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .18s ease}.smb-chat-header .smb-icon-btn:hover{background:#ffffff2e}.smb-icon-btn svg{display:block;pointer-events:none}.smb-menu-wrap{position:relative}.smb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:2;min-width:210px;padding:6px;background:var(--smb-bg);color:var(--smb-text);border-radius:14px;box-shadow:var(--smb-shadow)}.smb-commerce-widget-container .smb-menu button{display:block;width:100%;padding:10px 12px;border-radius:10px;text-align:left;font-size:14px}.smb-commerce-widget-container .smb-menu button:hover{background:var(--smb-bg-soft)}.smb-chat-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px;padding:16px 14px;background:var(--smb-bg)}.smb-commerce-widget-container .smb-ai-notice{align-self:center;max-width:92%;padding:6px 12px;border-radius:12px;background:var(--smb-bg-soft);color:var(--smb-text-soft);font-size:12px;text-align:center}.smb-msg{display:flex;max-width:86%;animation:smb-in .18s ease-out}.smb-msg.is-user{align-self:flex-end}.smb-msg.is-bot,.smb-msg.is-agent{align-self:flex-start}.smb-bubble{padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.is-user .smb-bubble{background:var(--smb-primary);color:var(--smb-on-primary);border-bottom-right-radius:6px}.is-bot .smb-bubble{background:var(--smb-bg-soft);color:var(--smb-text);border-bottom-left-radius:6px}.is-agent .smb-bubble{background:#e8efff;color:var(--smb-text);border-bottom-left-radius:6px}.smb-commerce-widget-container .message-link{color:#1d4ed8;text-decoration:underline;overflow-wrap:anywhere}.smb-commerce-widget-container .is-user .message-link{color:inherit}.smb-commerce-widget-container .message-image{display:block;max-width:100%;max-height:280px;width:auto;height:auto;margin:4px 0;border-radius:12px;object-fit:contain}.smb-commerce-widget-container .smb-md{white-space:normal}.smb-commerce-widget-container .smb-md>*+*{margin-top:8px}.smb-commerce-widget-container .smb-md p,.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol,.smb-commerce-widget-container .smb-md li,.smb-commerce-widget-container .smb-md blockquote,.smb-commerce-widget-container .smb-md pre,.smb-commerce-widget-container .smb-md table{margin:0;padding:0;border:0;font-size:inherit;line-height:inherit;color:inherit;background:none}.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol{padding-left:20px;list-style-position:outside}.smb-commerce-widget-container .smb-md ul{list-style-type:disc}.smb-commerce-widget-container .smb-md ul ul{list-style-type:circle}.smb-commerce-widget-container .smb-md ol{list-style-type:decimal}.smb-commerce-widget-container .smb-md li{display:list-item}.smb-commerce-widget-container .smb-md li+li,.smb-commerce-widget-container .smb-md li>ul,.smb-commerce-widget-container .smb-md li>ol{margin-top:4px}.smb-commerce-widget-container .smb-md strong{font-weight:700}.smb-commerce-widget-container .smb-md em{font-style:italic}.smb-commerce-widget-container .smb-md .smb-md-h{font-size:15px}.smb-commerce-widget-container .smb-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:#0f172a12;padding:1px 4px;border-radius:4px}.smb-commerce-widget-container .smb-md pre{padding:8px 10px;border-radius:8px;background:#0f172a12;overflow-x:auto;white-space:pre}.smb-commerce-widget-container .smb-md pre code{background:none;padding:0}.smb-commerce-widget-container .smb-md blockquote{padding-left:10px;border-left:3px solid rgba(15,23,42,.18);color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-md hr{height:0;margin:10px 0;border:0;border-top:1px solid rgba(15,23,42,.12)}.smb-commerce-widget-container .smb-md-table{overflow-x:auto}.smb-commerce-widget-container .smb-md table{border-collapse:collapse;width:100%;font-size:13px}.smb-commerce-widget-container .smb-md th,.smb-commerce-widget-container .smb-md td{padding:4px 8px;border:1px solid rgba(15,23,42,.12);text-align:left;vertical-align:top}.smb-commerce-widget-container .smb-md th{font-weight:700;background:#0f172a0a}.smb-typing{display:flex;align-items:center;gap:10px;color:var(--smb-text-soft);font-size:13px}.smb-dots{display:inline-flex;gap:4px}.smb-dots span{width:6px;height:6px;border-radius:50%;background:currentColor;animation:smb-dot 1.2s infinite ease-in-out both}.smb-dots span:nth-child(2){animation-delay:.15s}.smb-dots span:nth-child(3){animation-delay:.3s}@keyframes smb-dot{0%,80%,to{opacity:.25}40%{opacity:1}}.smb-card{padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-card p{margin-bottom:10px;font-weight:600}.smb-commerce-widget-container .smb-card-note{margin:0;font-weight:400;text-align:center;color:var(--smb-text-soft)}.smb-card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.smb-commerce-widget-container .smb-primary-btn,.smb-commerce-widget-container .smb-soft-btn{min-height:36px;padding:8px 16px;border-radius:999px;font-weight:600}.smb-commerce-widget-container .smb-primary-btn{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-soft-btn{background:var(--smb-bg);color:var(--smb-text)}.smb-commerce-widget-container .smb-text-btn{padding:8px;color:var(--smb-text-soft);text-decoration:underline}.smb-chips{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.smb-commerce-widget-container .smb-chip{padding:8px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;text-align:left;transition:background-color .18s ease}.smb-commerce-widget-container .smb-chip:hover{background:#e4e7ec}.smb-lead-form{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-lead-form h3{font-size:14px;font-weight:600}.smb-lead-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-lead-form input{width:100%;padding:10px 12px;border:0;border-radius:10px;background:var(--smb-bg);color:var(--smb-text);font-size:14px;box-shadow:none}.smb-commerce-widget-container .smb-error{margin-bottom:8px;padding:6px 10px;border-radius:10px;background:#fef3f2;color:#b42318;font-size:13px}.smb-chat-footer{flex-shrink:0;padding:8px 12px 10px;background:var(--smb-bg)}.smb-composer{display:flex;align-items:flex-end;gap:4px;padding:4px;border-radius:24px;background:var(--smb-bg-soft);transition:box-shadow .18s ease}.smb-composer:focus-within{box-shadow:0 0 0 2px var(--smb-focus)}.smb-composer .smb-icon-btn{color:var(--smb-text-soft)}.smb-composer .smb-icon-btn:hover:not(:disabled){background:#e4e7ec}.smb-commerce-widget-container .smb-composer .smb-mic{flex-shrink:0}.smb-commerce-widget-container .smb-composer .smb-mic.is-listening{color:#b42318;background:#fee4e2;animation:smb-pulse 1.4s ease-out infinite}@keyframes smb-pulse{0%{box-shadow:0 0 #b4231859}to{box-shadow:0 0 0 10px #b4231800}}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus{flex:1;min-width:0;min-height:36px;max-height:120px;padding:8px 4px;border:0;border-radius:0;outline:none;background:transparent;box-shadow:none;resize:none;color:var(--smb-text);font-size:14px;line-height:20px}.smb-composer textarea::placeholder{color:var(--smb-text-soft);opacity:1}.smb-commerce-widget-container .smb-send-btn{flex-shrink:0;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-footer-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.smb-footer-row:empty{display:none}.smb-commerce-widget-container .smb-human-btn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;font-weight:600}.smb-commerce-widget-container .smb-human-btn:hover:not(:disabled){background:#e4e7ec}.smb-channels{display:flex;gap:6px;margin-left:auto}.smb-commerce-widget-container .smb-channel{width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#fff;background:#374151}.smb-commerce-widget-container .smb-channel.is-whatsapp{background:#128c7e}.smb-commerce-widget-container .smb-channel.is-messenger{background:#0866ff}.smb-commerce-widget-container .smb-channel.is-telegram{background:#1c7fbf}.smb-commerce-widget-container .smb-channel.is-zalo{background:#0068ff}.smb-commerce-widget-container .smb-slogan{margin-top:6px;text-align:center;font-size:11px;color:var(--smb-text-soft)}.smb-sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.smb-commerce-widget-container.is-dodging .smb-launcher{opacity:0;pointer-events:none;visibility:hidden;transition:opacity .18s ease,visibility 0s linear .18s}@media (max-width: 767px){.smb-expand-btn{display:none!important}.smb-commerce-widget-container.is-commerce-page:not(.is-open){bottom:calc(96px + env(safe-area-inset-bottom,0px))}}@media (max-width: 480px){.smb-commerce-widget-container{right:12px;bottom:12px}.smb-commerce-widget-container.is-open{top:0;left:0;right:0;bottom:0}.smb-chat,.smb-chat.is-expanded{width:100vw;height:100vh;height:100dvh;border-radius:0;box-shadow:none}.smb-chat-header{padding-top:calc(12px + env(safe-area-inset-top,0px))}.smb-chat-footer{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px))}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus,.smb-commerce-widget-container .smb-lead-form input{font-size:16px}}@media (prefers-reduced-motion: reduce){.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{animation:none!important;transition:none!important}}
     1.smb-commerce-widget-container{--smb-bg: #ffffff;--smb-bg-soft: #f1f3f6;--smb-text: #1f2328;--smb-text-soft: #555b66;--smb-radius: 20px;--smb-shadow: 0 16px 40px rgba(15, 23, 42, .2), 0 2px 8px rgba(15, 23, 42, .08);--smb-focus: #2563eb;position:fixed;right:20px;bottom:20px;z-index:999999;min-width:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.45;color:var(--smb-text);text-align:left}.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{box-sizing:border-box!important}.smb-commerce-widget-container button,.smb-commerce-widget-container input,.smb-commerce-widget-container textarea{margin:0;font:inherit;letter-spacing:normal;text-transform:none}.smb-commerce-widget-container button,.smb-commerce-widget-container a{border:0;background:none;box-shadow:none;color:inherit;cursor:pointer;padding:0;min-height:0;text-decoration:none;line-height:1.2}.smb-commerce-widget-container button:disabled{cursor:default;opacity:.45}.smb-commerce-widget-container :focus-visible:not(textarea){outline:2px solid var(--smb-focus)!important;outline-offset:2px}.smb-commerce-widget-container h2,.smb-commerce-widget-container h3,.smb-commerce-widget-container p{margin:0;padding:0;font-family:inherit;letter-spacing:normal}.smb-commerce-widget-container .smb-launcher{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary);box-shadow:0 6px 20px #0f172a3d;transition:transform .18s ease}.smb-commerce-widget-container .smb-launcher:hover{transform:scale(1.06)}.smb-commerce-widget-container .smb-launcher{position:relative}.smb-commerce-widget-container .smb-unread{position:absolute;top:-4px;right:-4px;min-width:22px;height:22px;padding:0 6px;box-sizing:border-box;border-radius:11px;background:#d63638;color:#fff;border:2px solid #fff;font-size:12px;font-weight:600;line-height:18px;text-align:center}.smb-launcher img{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%}.smb-chat{width:380px;height:min(620px,calc(100vh - 40px));display:flex;flex-direction:column;overflow:hidden;background:var(--smb-bg);border-radius:var(--smb-radius);box-shadow:var(--smb-shadow);animation:smb-in .18s ease-out}.smb-chat.is-expanded{width:min(720px,calc(100vw - 40px));height:min(840px,calc(100vh - 40px))}@keyframes smb-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.smb-chat-header{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 10px 12px 16px;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-chat-identity{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.smb-chat-logo{flex-shrink:0;width:36px;height:36px;border-radius:50%;object-fit:cover;background:#ffffff38}.smb-chat-titles{min-width:0}.smb-commerce-widget-container .smb-chat-title{font-size:15px;font-weight:600;line-height:1.25;color:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.smb-commerce-widget-container .smb-chat-status{display:flex;align-items:center;gap:6px;font-size:12px;color:inherit;opacity:.92}.smb-status-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;background:#22c55e}.smb-status-dot.is-human_active{background:#f59e0b}.smb-status-dot.is-agent_active{background:#60a5fa}.smb-status-dot.is-closed{background:#9ca3af}.smb-chat-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.smb-commerce-widget-container .smb-icon-btn{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .18s ease}.smb-chat-header .smb-icon-btn:hover{background:#ffffff2e}.smb-icon-btn svg{display:block;pointer-events:none}.smb-menu-wrap{position:relative}.smb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:2;min-width:210px;padding:6px;background:var(--smb-bg);color:var(--smb-text);border-radius:14px;box-shadow:var(--smb-shadow)}.smb-commerce-widget-container .smb-menu button{display:block;width:100%;padding:10px 12px;border-radius:10px;text-align:left;font-size:14px}.smb-commerce-widget-container .smb-menu button:hover{background:var(--smb-bg-soft)}.smb-chat-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px;padding:16px 14px;background:var(--smb-bg)}.smb-commerce-widget-container .smb-ai-notice{align-self:center;max-width:92%;padding:6px 12px;border-radius:12px;background:var(--smb-bg-soft);color:var(--smb-text-soft);font-size:12px;text-align:center}.smb-msg{display:flex;max-width:86%;animation:smb-in .18s ease-out}.smb-msg.is-user{align-self:flex-end}.smb-msg.is-bot,.smb-msg.is-agent{align-self:flex-start}.smb-bubble{padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.is-user .smb-bubble{background:var(--smb-primary);color:var(--smb-on-primary);border-bottom-right-radius:6px}.is-bot .smb-bubble{background:var(--smb-bg-soft);color:var(--smb-text);border-bottom-left-radius:6px}.is-agent .smb-bubble{background:#e8efff;color:var(--smb-text);border-bottom-left-radius:6px}.smb-msg.smb-msg-cards{max-width:100%;width:100%;margin-top:-2px;flex-direction:column}.smb-cards{position:relative;width:100%}.smb-cards-track{display:flex;gap:8px;margin:0;padding:2px 2px 6px;list-style:none;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch}.smb-cards-track::-webkit-scrollbar{display:none}.smb-pcard{flex:0 0 82%;max-width:260px;scroll-snap-align:start;display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid rgba(15,23,42,.1);border-radius:14px;background:var(--smb-bg);color:var(--smb-text)}.smb-cards.is-single .smb-pcard{flex-basis:100%;max-width:100%}.smb-pcard-img{flex:0 0 64px;width:64px;height:64px;border-radius:10px;object-fit:cover;background:var(--smb-bg-soft)}.smb-pcard-body{min-width:0;display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:1.35}.smb-commerce-widget-container .smb-pcard-name{color:var(--smb-text);font-weight:600;text-decoration:none;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.smb-commerce-widget-container .smb-pcard-name:hover{text-decoration:underline}.smb-pcard-price{margin:0;font-weight:600}.smb-pcard-price del{margin-right:6px;color:var(--smb-text-soft);font-weight:400}.smb-pcard-stock{margin:0;font-size:12px;color:#15803d}.smb-pcard-stock.is-outofstock{color:#b42318}.smb-pcard-stock.is-onbackorder{color:#b45309}.smb-pcard-at{margin:0;font-size:11px;color:var(--smb-text-soft)}.smb-pcard-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap}.smb-commerce-widget-container .smb-pcard-btn{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;border-radius:999px;border:1px solid var(--smb-primary);color:var(--smb-primary);background:transparent;font-size:13px;font-weight:600;text-decoration:none;cursor:pointer}.smb-commerce-widget-container .smb-pcard-btn:hover{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-cards-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:1;width:30px;height:30px;border-radius:50%;border:0;cursor:pointer;background:var(--smb-bg);color:var(--smb-text);border:1px solid rgba(15,23,42,.12);box-shadow:0 2px 8px #0f172a2e;font-size:20px;font-weight:600;line-height:1}.smb-commerce-widget-container .smb-cards-nav.is-prev{left:-4px}.smb-commerce-widget-container .smb-cards-nav.is-next{right:-4px}.smb-pcard-btn:focus-visible,.smb-cards-nav:focus-visible,.smb-pcard-name:focus-visible{outline:2px solid var(--smb-focus);outline-offset:2px}@media (hover: none){.smb-commerce-widget-container .smb-cards-nav{display:none}}.smb-commerce-widget-container .smb-pcard-btn.is-primary{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-pcard-btn.is-primary:hover{filter:brightness(1.08)}.smb-commerce-widget-container .smb-pcard-btn[disabled]{opacity:.7;cursor:progress}.smb-commerce-widget-container .smb-pcard-link{color:var(--smb-primary);text-decoration:underline;font-size:13px}.smb-pcard-added{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px}.smb-pcard-added-text{color:#15803d;font-weight:600;font-size:13px}.smb-pcard-error{flex-basis:100%;color:#b42318;font-size:12px}.smb-commerce-widget-container .smb-cart-btn{position:relative}.smb-cart-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#fff;color:var(--smb-primary);font-size:10px;font-weight:700;line-height:16px;text-align:center}.smb-cart-panel{margin:0 0 8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg);border:1px solid rgba(15,23,42,.12);box-shadow:0 6px 18px #0f172a1f}.smb-cart-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.smb-cart-head h3{margin:0;font-size:15px;font-weight:700;color:var(--smb-text)}.smb-cart-head h3:focus{outline:none}.smb-cart-note{margin:4px 0;font-size:13px;color:var(--smb-text-soft)}.smb-cart-items{list-style:none;margin:0;padding:0}.smb-cart-item{display:flex;gap:8px;align-items:baseline;padding:6px 0;border-bottom:1px solid rgba(15,23,42,.08);font-size:13px}.smb-cart-name{flex:1;min-width:0;overflow-wrap:anywhere}.smb-cart-qty{color:var(--smb-text-soft);white-space:nowrap}.smb-cart-line{font-weight:600;white-space:nowrap}.smb-cart-subtotal{display:flex;justify-content:space-between;margin:8px 0;font-size:14px}.smb-cart-options{display:block;font-size:12px;color:var(--smb-text-soft)}.smb-ocards{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;width:100%}.smb-ocard{padding:10px 12px;border:1px solid rgba(15,23,42,.1);border-radius:14px;background:var(--smb-bg);color:var(--smb-text);font-size:13px;line-height:1.4}.smb-ocard-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:4px 8px}.smb-ocard-status{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600;background:var(--smb-bg-soft);color:var(--smb-text)}.smb-ocard-status.is-processing,.smb-ocard-status.is-on-hold{background:#fef3c7;color:#92400e}.smb-ocard-status.is-completed{background:#dcfce7;color:#166534}.smb-ocard-status.is-cancelled,.smb-ocard-status.is-failed,.smb-ocard-status.is-refunded{background:#fee2e2;color:#991b1b}.smb-ocard-meta{margin:4px 0 0;color:var(--smb-text-soft);font-size:12px}.smb-commerce-widget-container .smb-ocard-items{list-style:none;margin:6px 0 0;padding:0}.smb-ocard-items li{overflow-wrap:anywhere}.smb-ocard-more{color:var(--smb-text-soft)}.smb-ocard-track{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px;margin:8px 0 0}.smb-ocard-track code{font-size:12px;overflow-wrap:anywhere}.smb-ocard .smb-pcard-link{display:inline-block;margin-top:6px}.smb-cart-discount{display:flex;justify-content:space-between;margin:-4px 0 8px;font-size:13px;color:#15803d}.smb-cart-actions{display:flex;gap:8px;flex-wrap:wrap}.smb-commerce-widget-container .message-link{color:#1d4ed8;text-decoration:underline;overflow-wrap:anywhere}.smb-commerce-widget-container .is-user .message-link{color:inherit}.smb-commerce-widget-container .message-image{display:block;max-width:100%;max-height:280px;width:auto;height:auto;margin:4px 0;border-radius:12px;object-fit:contain}.smb-commerce-widget-container .smb-md{white-space:normal}.smb-commerce-widget-container .smb-md>*+*{margin-top:8px}.smb-commerce-widget-container .smb-md p,.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol,.smb-commerce-widget-container .smb-md li,.smb-commerce-widget-container .smb-md blockquote,.smb-commerce-widget-container .smb-md pre,.smb-commerce-widget-container .smb-md table{margin:0;padding:0;border:0;font-size:inherit;line-height:inherit;color:inherit;background:none}.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol{padding-left:20px;list-style-position:outside}.smb-commerce-widget-container .smb-md ul{list-style-type:disc}.smb-commerce-widget-container .smb-md ul ul{list-style-type:circle}.smb-commerce-widget-container .smb-md ol{list-style-type:decimal}.smb-commerce-widget-container .smb-md li{display:list-item}.smb-commerce-widget-container .smb-md li+li,.smb-commerce-widget-container .smb-md li>ul,.smb-commerce-widget-container .smb-md li>ol{margin-top:4px}.smb-commerce-widget-container .smb-md strong{font-weight:700}.smb-commerce-widget-container .smb-md em{font-style:italic}.smb-commerce-widget-container .smb-md .smb-md-h{font-size:15px}.smb-commerce-widget-container .smb-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:#0f172a12;padding:1px 4px;border-radius:4px}.smb-commerce-widget-container .smb-md pre{padding:8px 10px;border-radius:8px;background:#0f172a12;overflow-x:auto;white-space:pre}.smb-commerce-widget-container .smb-md pre code{background:none;padding:0}.smb-commerce-widget-container .smb-md blockquote{padding-left:10px;border-left:3px solid rgba(15,23,42,.18);color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-md hr{height:0;margin:10px 0;border:0;border-top:1px solid rgba(15,23,42,.12)}.smb-commerce-widget-container .smb-md-table{overflow-x:auto}.smb-commerce-widget-container .smb-md table{border-collapse:collapse;width:100%;font-size:13px}.smb-commerce-widget-container .smb-md th,.smb-commerce-widget-container .smb-md td{padding:4px 8px;border:1px solid rgba(15,23,42,.12);text-align:left;vertical-align:top}.smb-commerce-widget-container .smb-md th{font-weight:700;background:#0f172a0a}.smb-typing{display:flex;align-items:center;gap:10px;color:var(--smb-text-soft);font-size:13px}.smb-dots{display:inline-flex;gap:4px}.smb-dots span{width:6px;height:6px;border-radius:50%;background:currentColor;animation:smb-dot 1.2s infinite ease-in-out both}.smb-dots span:nth-child(2){animation-delay:.15s}.smb-dots span:nth-child(3){animation-delay:.3s}@keyframes smb-dot{0%,80%,to{opacity:.25}40%{opacity:1}}.smb-card{padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-card p{margin-bottom:10px;font-weight:600}.smb-commerce-widget-container .smb-card-note{margin:0;font-weight:400;text-align:center;color:var(--smb-text-soft)}.smb-card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.smb-commerce-widget-container .smb-primary-btn,.smb-commerce-widget-container .smb-soft-btn{min-height:36px;padding:8px 16px;border-radius:999px;font-weight:600}.smb-commerce-widget-container .smb-primary-btn{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-soft-btn{background:var(--smb-bg);color:var(--smb-text)}.smb-commerce-widget-container .smb-text-btn{padding:8px;color:var(--smb-text-soft);text-decoration:underline}.smb-chips{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.smb-commerce-widget-container .smb-chip{padding:8px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;text-align:left;transition:background-color .18s ease}.smb-commerce-widget-container .smb-chip:hover{background:#e4e7ec}.smb-lead-form{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-lead-form h3{font-size:14px;font-weight:600}.smb-lead-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-lead-form input{width:100%;padding:10px 12px;border:0;border-radius:10px;background:var(--smb-bg);color:var(--smb-text);font-size:14px;box-shadow:none}.smb-commerce-widget-container .smb-error{margin-bottom:8px;padding:6px 10px;border-radius:10px;background:#fef3f2;color:#b42318;font-size:13px}.smb-chat-footer{flex-shrink:0;padding:8px 12px 10px;background:var(--smb-bg)}.smb-composer{display:flex;align-items:flex-end;gap:4px;padding:4px;border-radius:24px;background:var(--smb-bg-soft);transition:box-shadow .18s ease}.smb-composer:focus-within{box-shadow:0 0 0 2px var(--smb-focus)}.smb-composer .smb-icon-btn{color:var(--smb-text-soft)}.smb-composer .smb-icon-btn:hover:not(:disabled){background:#e4e7ec}.smb-commerce-widget-container .smb-composer .smb-mic{flex-shrink:0}.smb-commerce-widget-container .smb-composer .smb-mic.is-listening{color:#b42318;background:#fee4e2;animation:smb-pulse 1.4s ease-out infinite}@keyframes smb-pulse{0%{box-shadow:0 0 #b4231859}to{box-shadow:0 0 0 10px #b4231800}}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus{flex:1;min-width:0;min-height:36px;max-height:120px;padding:8px 4px;border:0;border-radius:0;outline:none;background:transparent;box-shadow:none;resize:none;color:var(--smb-text);font-size:14px;line-height:20px}.smb-composer textarea::placeholder{color:var(--smb-text-soft);opacity:1}.smb-commerce-widget-container .smb-send-btn{flex-shrink:0;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-footer-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.smb-footer-row:empty{display:none}.smb-commerce-widget-container .smb-human-btn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;font-weight:600}.smb-commerce-widget-container .smb-human-btn:hover:not(:disabled){background:#e4e7ec}.smb-channels{display:flex;gap:6px;margin-left:auto}.smb-commerce-widget-container .smb-channel{width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#fff;background:#374151}.smb-commerce-widget-container .smb-channel.is-whatsapp{background:#128c7e}.smb-commerce-widget-container .smb-channel.is-messenger{background:#0866ff}.smb-commerce-widget-container .smb-channel.is-telegram{background:#1c7fbf}.smb-commerce-widget-container .smb-channel.is-zalo{background:#0068ff}.smb-commerce-widget-container .smb-slogan{margin-top:6px;text-align:center;font-size:11px;color:var(--smb-text-soft)}.smb-sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.smb-commerce-widget-container.is-dodging .smb-launcher{opacity:0;pointer-events:none;visibility:hidden;transition:opacity .18s ease,visibility 0s linear .18s}@media (max-width: 767px){.smb-expand-btn{display:none!important}.smb-commerce-widget-container.is-commerce-page:not(.is-open){bottom:calc(96px + env(safe-area-inset-bottom,0px))}}@media (max-width: 480px){.smb-commerce-widget-container{right:12px;bottom:12px}.smb-commerce-widget-container.is-open{top:0;left:0;right:0;bottom:0}.smb-chat,.smb-chat.is-expanded{width:100vw;height:100vh;height:100dvh;border-radius:0;box-shadow:none}.smb-chat-header{padding-top:calc(12px + env(safe-area-inset-top,0px))}.smb-chat-footer{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px))}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus,.smb-commerce-widget-container .smb-lead-form input{font-size:16px}}@media (prefers-reduced-motion: reduce){.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{animation:none!important;transition:none!important}}
  • ninhle-ai-sales-assistant/tags/2.6.0/languages/ninhle-ai-sales-assistant.pot

    r3761175 r3765552  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Ninhle AI Sales Assistant for WooCommerce 2.5.0\n"
     5"Project-Id-Version: Ninhle AI Sales Assistant – AI Chatbot for WooCommerce 2.6.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ninhle-ai-sales-assistant\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-23T06:16:45+00:00\n"
     12"POT-Creation-Date: 2026-10-26T03:45:21+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1717#. Plugin Name of the plugin
    1818#: ninhle-ai-sales-assistant.php
    19 msgid "Ninhle AI Sales Assistant for WooCommerce"
     19msgid "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce"
    2020msgstr ""
    2121
    2222#. Description of the plugin
    2323#: ninhle-ai-sales-assistant.php
    24 msgid "AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff."
     24msgid "AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in the chat, tracks orders and shows why shoppers don't buy."
    2525msgstr ""
    2626
    … …  
    3535msgstr ""
    3636
    37 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:26
     37#. translators: %s: discount percentage, e.g. "10".
     38#: premium/Commerce/Woo/CartActionService.php:376
     39#, php-format
     40msgid "%s%% off"
     41msgstr ""
     42
     43#. translators: %s: discount amount with currency.
     44#: premium/Commerce/Woo/CartActionService.php:381
     45#, php-format
     46msgid "%s off each item"
     47msgstr ""
     48
     49#. translators: %s: discount amount with currency.
     50#: premium/Commerce/Woo/CartActionService.php:384
     51#, php-format
     52msgid "%s off your order"
     53msgstr ""
     54
     55#: premium/Commerce/Woo/CartActionService.php:471
     56msgid "Quantity"
     57msgstr ""
     58
     59#: premium/Commerce/Woo/CartActionService.php:472
     60msgid "Decrease quantity"
     61msgstr ""
     62
     63#: premium/Commerce/Woo/CartActionService.php:473
     64msgid "Increase quantity"
     65msgstr ""
     66
     67#. translators: %s: product name.
     68#: premium/Commerce/Woo/CartActionService.php:475
     69#, php-format
     70msgid "Remove %s"
     71msgstr ""
     72
     73#: premium/Commerce/Woo/CartActionService.php:476
     74msgid "Remove"
     75msgstr ""
     76
     77#: premium/Commerce/Woo/CartActionService.php:477
     78msgid "Choose your options to add this to the cart."
     79msgstr ""
     80
     81#: premium/Commerce/Woo/CartActionService.php:478
     82msgid "sold out"
     83msgstr ""
     84
     85#: premium/Commerce/Woo/CartActionService.php:479
     86msgid "This combination is not available."
     87msgstr ""
     88
     89#. translators: %d: largest quantity that can be added.
     90#: premium/Commerce/Woo/CartActionService.php:481
     91#, php-format
     92msgid "Up to %d"
     93msgstr ""
     94
     95#: premium/Commerce/Woo/CartActionService.php:482
     96msgid "Updating your cart…"
     97msgstr ""
     98
     99#: premium/Commerce/Woo/CartActionService.php:483
     100msgid "Couldn't update your cart."
     101msgstr ""
     102
     103#: premium/Commerce/Woo/CartActionService.php:484
     104msgid "Apply"
     105msgstr ""
     106
     107#: premium/Commerce/Woo/CartActionService.php:485
     108msgid "Applying…"
     109msgstr ""
     110
     111#. translators: %s: coupon code.
     112#: premium/Commerce/Woo/CartActionService.php:487
     113#, php-format
     114msgid "Code %s applied"
     115msgstr ""
     116
     117#: premium/Commerce/Woo/CartActionService.php:488
     118msgid "Couldn't apply this code."
     119msgstr ""
     120
     121#. translators: %s: minimum order amount with currency.
     122#: premium/Commerce/Woo/CartActionService.php:490
     123#, php-format
     124msgid "Minimum order %s"
     125msgstr ""
     126
     127#: premium/Commerce/Woo/CartActionService.php:491
     128msgid "Discount codes"
     129msgstr ""
     130
     131#: premium/CRM/CrmStore.php:609
     132msgid "Customer ID"
     133msgstr ""
     134
     135#: premium/CRM/CrmStore.php:610
     136#: src/PHP/Privacy/PersonalData.php:249
     137#: src/PHP/REST/LeadController.php:145
     138#: src/PHP/Widget/WidgetLoader.php:94
     139msgid "Name"
     140msgstr ""
     141
     142#: premium/CRM/CrmStore.php:611
     143msgid "Verified"
     144msgstr ""
     145
     146#: premium/CRM/CrmStore.php:611
     147#: src/PHP/Widget/WidgetLoader.php:155
     148msgid "Yes"
     149msgstr ""
     150
     151#: premium/CRM/CrmStore.php:611
     152msgid "No"
     153msgstr ""
     154
     155#. translators: %s: contact type, for example "email" or "phone".
     156#: premium/CRM/CrmStore.php:616
     157#, php-format
     158msgid "Contact (%s)"
     159msgstr ""
     160
     161#: premium/CRM/CrmStore.php:620
     162msgid "Ninhle AI Sales Assistant customer profile"
     163msgstr ""
     164
     165#: premium/CRM/CrmStore.php:623
     166msgid "Ninhle AI Sales Assistant contact permissions"
     167msgstr ""
     168
     169#: premium/CRM/CrmStore.php:624
     170msgid "Purpose"
     171msgstr ""
     172
     173#: premium/CRM/CrmStore.php:625
     174#: premium/CRM/CrmStore.php:666
     175#: src/PHP/Privacy/PersonalData.php:296
     176msgid "Channel"
     177msgstr ""
     178
     179#: premium/CRM/CrmStore.php:626
     180#: premium/CRM/CrmStore.php:676
     181#: src/PHP/Privacy/PersonalData.php:252
     182#: src/PHP/REST/LeadController.php:148
     183msgid "Status"
     184msgstr ""
     185
     186#: premium/CRM/CrmStore.php:627
     187msgid "Source"
     188msgstr ""
     189
     190#: premium/CRM/CrmStore.php:628
     191msgid "Recorded"
     192msgstr ""
     193
     194#: premium/CRM/CrmStore.php:634
     195msgid "Ninhle AI Sales Assistant purchase opportunities"
     196msgstr ""
     197
     198#: premium/CRM/CrmStore.php:635
     199msgid "Need"
     200msgstr ""
     201
     202#: premium/CRM/CrmStore.php:636
     203msgid "Stage"
     204msgstr ""
     205
     206#: premium/CRM/CrmStore.php:637
     207msgid "Payment"
     208msgstr ""
     209
     210#: premium/CRM/CrmStore.php:638
     211#: src/PHP/Widget/WidgetLoader.php:118
     212msgid "Products"
     213msgstr ""
     214
     215#: premium/CRM/CrmStore.php:639
     216#: src/PHP/Admin/AdminMenu.php:215
     217msgid "Orders"
     218msgstr ""
     219
     220#: premium/CRM/CrmStore.php:640
     221#: premium/CRM/CrmStore.php:678
     222#: src/PHP/Privacy/PersonalData.php:254
     223#: src/PHP/REST/LeadController.php:150
     224msgid "Created"
     225msgstr ""
     226
     227#: premium/CRM/CrmStore.php:664
     228msgid "Ninhle AI Sales Assistant customer history"
     229msgstr ""
     230
     231#: premium/CRM/CrmStore.php:665
     232#: premium/CRM/CrmStore.php:675
     233msgid "Type"
     234msgstr ""
     235
     236#: premium/CRM/CrmStore.php:667
     237msgid "Content"
     238msgstr ""
     239
     240#: premium/CRM/CrmStore.php:668
     241#: src/PHP/Privacy/PersonalData.php:337
     242msgid "When"
     243msgstr ""
     244
     245#: premium/CRM/CrmStore.php:674
     246msgid "Ninhle AI Sales Assistant support and follow-up requests"
     247msgstr ""
     248
     249#: premium/CRM/CrmStore.php:677
     250msgid "Resolution"
     251msgstr ""
     252
     253#. translators: %d: number of chats
     254#: premium/Insights/InsightsDigest.php:66
     255#, php-format
     256msgid "%d chat with the assistant in the last 7 days."
     257msgid_plural "%d chats with the assistant in the last 7 days."
     258msgstr[0] ""
     259msgstr[1] ""
     260
     261#: premium/Insights/InsightsDigest.php:69
     262msgid "Questions the assistant could not answer:"
     263msgstr ""
     264
     265#: premium/Insights/InsightsDigest.php:72
     266#: premium/Insights/InsightsDigest.php:94
     267msgid "None this week."
     268msgstr ""
     269
     270#. translators: 1: customer question, 2: number of times asked
     271#: premium/Insights/InsightsDigest.php:76
     272#, php-format
     273msgid "- \"%1$s\" (%2$d×)"
     274msgstr ""
     275
     276#: premium/Insights/InsightsDigest.php:80
     277msgid "What customers looked for but could not get:"
     278msgstr ""
     279
     280#. translators: 1: product name, 2: number of times asked
     281#: premium/Insights/InsightsDigest.php:84
     282#, php-format
     283msgid "- %1$s: out of stock (%2$d×)"
     284msgstr ""
     285
     286#. translators: 1: search words, 2: number of searches
     287#: premium/Insights/InsightsDigest.php:88
     288#, php-format
     289msgid "- \"%1$s\": no matching product (%2$d×)"
     290msgstr ""
     291
     292#. translators: 1: reason, 2: percentage, 3: number of conversations labeled
     293#: premium/Insights/InsightsDigest.php:104
     294#, php-format
     295msgid "Most common reason shoppers did not buy: %1$s (%2$s%% of %3$d chats)."
     296msgstr ""
     297
     298#. translators: %s: amount in US dollars
     299#: premium/Insights/InsightsDigest.php:110
     300#, php-format
     301msgid "AI cost in the last 7 days: %s"
     302msgstr ""
     303
     304#. translators: %s: link to the Sales Insights tab
     305#: premium/Insights/InsightsDigest.php:113
     306#, php-format
     307msgid "Open Sales Insights: %s"
     308msgstr ""
     309
     310#: premium/Insights/InsightsDigest.php:114
     311msgid "You can turn this email off in Sales Insights > Settings."
     312msgstr ""
     313
     314#. translators: %s: site name
     315#: premium/Insights/InsightsDigest.php:118
     316#, php-format
     317msgid "[%s] Your weekly Sales Insights"
     318msgstr ""
     319
     320#: premium/Insights/InsightsDigest.php:125
     321msgid "price"
     322msgstr ""
     323
     324#: premium/Insights/InsightsDigest.php:126
     325msgid "shipping cost or time"
     326msgstr ""
     327
     328#: premium/Insights/InsightsDigest.php:127
     329msgid "unsure about size or fit"
     330msgstr ""
     331
     332#: premium/Insights/InsightsDigest.php:128
     333msgid "out of stock"
     334msgstr ""
     335
     336#: premium/Insights/InsightsDigest.php:129
     337msgid "missing information"
     338msgstr ""
     339
     340#: premium/Insights/InsightsDigest.php:130
     341msgid "waiting for a discount"
     342msgstr ""
     343
     344#: premium/Insights/InsightsDigest.php:131
     345msgid "just browsing"
     346msgstr ""
     347
     348#: premium/Insights/InsightsDigest.php:132
     349msgid "other"
     350msgstr ""
     351
     352#: premium/Integrations/TelegramService.php:77
     353msgid "None"
     354msgstr ""
     355
     356#. translators: %s: chat widget brand name.
     357#: premium/Integrations/TelegramService.php:79
     358#, php-format
     359msgid "New lead from %s"
     360msgstr ""
     361
     362#: premium/Integrations/TelegramService.php:80
     363msgid "Name:"
     364msgstr ""
     365
     366#: premium/Integrations/TelegramService.php:81
     367msgid "Email:"
     368msgstr ""
     369
     370#: premium/Integrations/TelegramService.php:82
     371msgid "Phone:"
     372msgstr ""
     373
     374#: premium/REST/TelegramController.php:55
     375#: src/PHP/Core/JobDispatcher.php:99
     376msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly."
     377msgstr ""
     378
     379#: premium/Webhooks/WebhookDispatcher.php:113
     380msgid "Outgoing webhooks are available in the Pro plan."
     381msgstr ""
     382
     383#: src/PHP/Abilities/AbilitiesService.php:57
     384msgid "Store catalog"
     385msgstr ""
     386
     387#: src/PHP/Abilities/AbilitiesService.php:58
     388msgid "Read-only access to the public product catalog and store policies."
     389msgstr ""
     390
     391#: src/PHP/Abilities/AbilitiesService.php:75
     392msgid "Search products"
     393msgstr ""
     394
     395#: src/PHP/Abilities/AbilitiesService.php:76
     396msgid "Searches the public product catalog of this store. Returns up to 10 products with price, currency, stock status, link and image."
     397msgstr ""
     398
     399#: src/PHP/Abilities/AbilitiesService.php:104
     400msgid "Get product details"
     401msgstr ""
     402
     403#: src/PHP/Abilities/AbilitiesService.php:105
     404msgid "Returns public details of one product: price, stock, description, categories, attributes and, for products with options, each option with its price and stock."
     405msgstr ""
     406
     407#: src/PHP/Abilities/AbilitiesService.php:120
     408msgid "Get store policies"
     409msgstr ""
     410
     411#: src/PHP/Abilities/AbilitiesService.php:121
     412msgid "Returns the store delivery, refund and warranty policies and the payment methods it accepts."
     413msgstr ""
     414
     415#: src/PHP/Abilities/AbilitiesService.php:153
     416msgid "Too many requests. Try again later."
     417msgstr ""
     418
     419#: src/PHP/Abilities/AbilitiesService.php:197
     420msgid "Product not found."
     421msgstr ""
     422
    38423#: src/PHP/Admin/AdminMenu.php:26
    39424msgid "Ninhle AI Sales Assistant"
    40425msgstr ""
    41426
    42 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:27
    43427#: src/PHP/Admin/AdminMenu.php:27
    44428msgid "Ninhle AI Sales"
    45429msgstr ""
    46430
    47 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:91
    48431#: src/PHP/Admin/AdminMenu.php:91
     432msgid "Sales Insights"
     433msgstr ""
     434
     435#: src/PHP/Admin/AdminMenu.php:92
     436msgid "What shoppers asked that the assistant could not answer, what they looked for but could not get, and why they did not buy. Contact details are hidden."
     437msgstr ""
     438
     439#. translators: %s: number of chats
     440#: src/PHP/Admin/AdminMenu.php:94
     441#, php-format
     442msgid "%s chats in this period"
     443msgstr ""
     444
     445#. translators: 1: number of chats so far, 2: number of chats needed
     446#: src/PHP/Admin/AdminMenu.php:96
     447#, php-format
     448msgid "Not enough data yet (%1$s of %2$s chats); the lists below are a first look."
     449msgstr ""
     450
     451#: src/PHP/Admin/AdminMenu.php:98
     452msgid "Questions the assistant could not answer"
     453msgstr ""
     454
     455#: src/PHP/Admin/AdminMenu.php:99
     456msgid "Similar questions are grouped. Add an answer to teach the assistant, or dismiss questions you do not want to answer. A question shows again if shoppers keep asking it after you added an answer."
     457msgstr ""
     458
     459#: src/PHP/Admin/AdminMenu.php:100
     460msgid "No unanswered questions in this period."
     461msgstr ""
     462
     463#: src/PHP/Admin/AdminMenu.php:101
     464#: src/PHP/Admin/AdminMenu.php:116
     465msgid "Question"
     466msgstr ""
     467
     468#: src/PHP/Admin/AdminMenu.php:102
     469msgid "Times asked"
     470msgstr ""
     471
     472#: src/PHP/Admin/AdminMenu.php:103
     473msgid "Last asked"
     474msgstr ""
     475
     476#: src/PHP/Admin/AdminMenu.php:104
     477msgid "Actions"
     478msgstr ""
     479
     480#. translators: %s: other ways the question was asked
     481#: src/PHP/Admin/AdminMenu.php:106
     482#, php-format
     483msgid "Also asked as: %s"
     484msgstr ""
     485
     486#: src/PHP/Admin/AdminMenu.php:107
     487msgid "shopper said it was not answered"
     488msgstr ""
     489
     490#: src/PHP/Admin/AdminMenu.php:108
     491msgid "information missing"
     492msgstr ""
     493
     494#: src/PHP/Admin/AdminMenu.php:109
     495msgid "no matching product"
     496msgstr ""
     497
     498#: src/PHP/Admin/AdminMenu.php:110
     499msgid "passed to your team"
     500msgstr ""
     501
     502#: src/PHP/Admin/AdminMenu.php:111
     503msgid "local-only mode"
     504msgstr ""
     505
     506#: src/PHP/Admin/AdminMenu.php:112
     507msgid "Add answer"
     508msgstr ""
     509
     510#: src/PHP/Admin/AdminMenu.php:113
     511msgid "Dismiss"
     512msgstr ""
     513
     514#: src/PHP/Admin/AdminMenu.php:114
     515msgid "Question dismissed."
     516msgstr ""
     517
     518#: src/PHP/Admin/AdminMenu.php:115
     519msgid "Open a chat"
     520msgstr ""
     521
     522#: src/PHP/Admin/AdminMenu.php:117
     523msgid "Answer"
     524msgstr ""
     525
     526#: src/PHP/Admin/AdminMenu.php:118
     527msgid "Where to save the answer"
     528msgstr ""
     529
     530#: src/PHP/Admin/AdminMenu.php:119
     531msgid "Add to Business information (the assistant uses it from the next reply)"
     532msgstr ""
     533
     534#: src/PHP/Admin/AdminMenu.php:120
     535msgid "Save as a note so I can update my policy pages"
     536msgstr ""
     537
     538#: src/PHP/Admin/AdminMenu.php:121
     539msgid "Save answer"
     540msgstr ""
     541
     542#: src/PHP/Admin/AdminMenu.php:122
     543#: src/PHP/Widget/WidgetLoader.php:81
     544msgid "Cancel"
     545msgstr ""
     546
     547#: src/PHP/Admin/AdminMenu.php:123
     548msgid "Answer added to Business information."
     549msgstr ""
     550
     551#: src/PHP/Admin/AdminMenu.php:124
     552msgid "Answer saved as a note."
     553msgstr ""
     554
     555#: src/PHP/Admin/AdminMenu.php:125
     556msgid "Could not save the answer. Enter both the question and the answer."
     557msgstr ""
     558
     559#: src/PHP/Admin/AdminMenu.php:126
     560msgid "Notes for your policy pages"
     561msgstr ""
     562
     563#: src/PHP/Admin/AdminMenu.php:127
     564msgid "Answers you chose to add to your own pages. The assistant does not read these notes."
     565msgstr ""
     566
     567#: src/PHP/Admin/AdminMenu.php:128
     568msgid "Delete"
     569msgstr ""
     570
     571#: src/PHP/Admin/AdminMenu.php:130
     572msgid "What shoppers could not find"
     573msgstr ""
     574
     575#: src/PHP/Admin/AdminMenu.php:131
     576msgid "Searches with no matching product"
     577msgstr ""
     578
     579#: src/PHP/Admin/AdminMenu.php:132
     580msgid "Wanted but out of stock"
     581msgstr ""
     582
     583#: src/PHP/Admin/AdminMenu.php:133
     584msgid "None in this period."
     585msgstr ""
     586
     587#: src/PHP/Admin/AdminMenu.php:134
     588msgid "Search your products"
     589msgstr ""
     590
     591#: src/PHP/Admin/AdminMenu.php:135
     592msgid "In stock now"
     593msgstr ""
     594
     595#: src/PHP/Admin/AdminMenu.php:136
     596msgid "Backorders allowed"
     597msgstr ""
     598
     599#: src/PHP/Admin/AdminMenu.php:137
     600msgid "Restock it, or allow backorders under Product data > Inventory."
     601msgstr ""
     602
     603#: src/PHP/Admin/AdminMenu.php:139
     604msgid "Why shoppers did not buy"
     605msgstr ""
     606
     607#: src/PHP/Admin/AdminMenu.php:140
     608msgid "Available in the Starter plan: each night an AI reads finished chats that did not lead to an order and sorts them by reason (price, shipping, size, stock, missing information and more), with example quotes."
     609msgstr ""
     610
     611#: src/PHP/Admin/AdminMenu.php:141
     612msgid "Turn on AI labeling below to see why shoppers did not buy."
     613msgstr ""
     614
     615#: src/PHP/Admin/AdminMenu.php:142
     616msgid "AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on."
     617msgstr ""
     618
     619#. translators: 1: chats labeled so far, 2: chats needed
     620#: src/PHP/Admin/AdminMenu.php:144
     621#, php-format
     622msgid "Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough."
     623msgstr ""
     624
     625#: src/PHP/Admin/AdminMenu.php:145
     626msgid "Price"
     627msgstr ""
     628
     629#: src/PHP/Admin/AdminMenu.php:146
     630msgid "Shipping cost or time"
     631msgstr ""
     632
     633#: src/PHP/Admin/AdminMenu.php:147
     634msgid "Unsure about size or fit"
     635msgstr ""
     636
     637#: src/PHP/Admin/AdminMenu.php:148
     638#: src/PHP/Widget/WidgetLoader.php:123
     639msgid "Out of stock"
     640msgstr ""
     641
     642#: src/PHP/Admin/AdminMenu.php:149
     643msgid "Missing information"
     644msgstr ""
     645
     646#: src/PHP/Admin/AdminMenu.php:150
     647msgid "Waiting for a discount"
     648msgstr ""
     649
     650#: src/PHP/Admin/AdminMenu.php:151
     651msgid "Just browsing"
     652msgstr ""
     653
     654#: src/PHP/Admin/AdminMenu.php:152
     655msgid "Other"
     656msgstr ""
     657
     658#: src/PHP/Admin/AdminMenu.php:153
     659msgid "See the unanswered questions"
     660msgstr ""
     661
     662#. translators: 1: AI provider and model, 2: amount spent this month, 3: monthly limit
     663#: src/PHP/Admin/AdminMenu.php:155
     664#, php-format
     665msgid "Model: %1$s · This month: %2$s of %3$s"
     666msgstr ""
     667
     668#. translators: %s: date
     669#: src/PHP/Admin/AdminMenu.php:157
     670#, php-format
     671msgid "Last run: %s"
     672msgstr ""
     673
     674#: src/PHP/Admin/AdminMenu.php:158
     675msgid "Stopped: the monthly limit for AI labeling was reached."
     676msgstr ""
     677
     678#: src/PHP/Admin/AdminMenu.php:159
     679msgid "Stopped: the overall AI budget was reached."
     680msgstr ""
     681
     682#: src/PHP/Admin/AdminMenu.php:160
     683msgid "Stopped: no AI provider with an API key is set up."
     684msgstr ""
     685
     686#: src/PHP/Admin/AdminMenu.php:161
     687msgid "Paused: 200 chats are labeled per night at most."
     688msgstr ""
     689
     690#: src/PHP/Admin/AdminMenu.php:163
     691msgid "Sales Insights settings"
     692msgstr ""
     693
     694#: src/PHP/Admin/AdminMenu.php:164
     695msgid "Label why shoppers did not buy, each night with AI"
     696msgstr ""
     697
     698#: src/PHP/Admin/AdminMenu.php:165
     699msgid "Runs at 2:00 site time on up to 200 finished chats that did not lead to an order, using your own AI provider key. Email addresses and phone numbers are hidden before anything is sent. Off by default."
     700msgstr ""
     701
     702#: src/PHP/Admin/AdminMenu.php:166
     703msgid "Monthly limit (USD)"
     704msgstr ""
     705
     706#: src/PHP/Admin/AdminMenu.php:167
     707msgid "Model"
     708msgstr ""
     709
     710#: src/PHP/Admin/AdminMenu.php:168
     711msgid "Cheapest available (recommended)"
     712msgstr ""
     713
     714#: src/PHP/Admin/AdminMenu.php:169
     715msgid "Same as the chat assistant"
     716msgstr ""
     717
     718#: src/PHP/Admin/AdminMenu.php:170
     719msgid "Email me a weekly summary"
     720msgstr ""
     721
     722#: src/PHP/Admin/AdminMenu.php:171
     723msgid "Every Monday morning to the site admin email: top unanswered questions, what shoppers could not find, the main reason they did not buy and the AI cost of the week. Not sent in a week without chats."
     724msgstr ""
     725
     726#: src/PHP/Admin/AdminMenu.php:178
    49727msgid "AI Value & Performance"
    50728msgstr ""
    51729
    52 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:92
    53 #: src/PHP/Admin/AdminMenu.php:92
     730#: src/PHP/Admin/AdminMenu.php:179
    54731msgid "What the assistant cost, the orders it helped with and how conversations ended. Figures start on the measurement start date and are never backfilled."
    55732msgstr ""
    56733
    57 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:93
    58 #: src/PHP/Admin/AdminMenu.php:93
     734#: src/PHP/Admin/AdminMenu.php:180
    59735msgid "Report period"
    60736msgstr ""
    61737
    62738#. translators: %s: number of days
    63 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:95
    64 #: src/PHP/Admin/AdminMenu.php:95
     739#: src/PHP/Admin/AdminMenu.php:182
    65740#, php-format
    66741msgid "Last %s days"
    … …  
    68743
    69744#. translators: %s: date the plugin started measuring
    70 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:97
    71 #: src/PHP/Admin/AdminMenu.php:97
     745#: src/PHP/Admin/AdminMenu.php:184
    72746#, php-format
    73747msgid "Measured since %s"
    74748msgstr ""
    75749
    76 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:98
    77 #: src/PHP/Admin/AdminMenu.php:98
     750#: src/PHP/Admin/AdminMenu.php:185
    78751msgid "Measurement starts after the database update to version 2.4.0."
    79752msgstr ""
    80753
    81 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:99
    82 #: src/PHP/Admin/AdminMenu.php:99
     754#: src/PHP/Admin/AdminMenu.php:186
    83755msgid "Loading the figures…"
    84756msgstr ""
    85757
    86 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:100
    87 #: src/PHP/Admin/AdminMenu.php:100
     758#: src/PHP/Admin/AdminMenu.php:187
    88759msgid "Could not load the figures. Please reload the page."
    89760msgstr ""
    90761
    91762#. translators: %s: sample size
    92 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:102
    93 #: src/PHP/Admin/AdminMenu.php:102
     763#: src/PHP/Admin/AdminMenu.php:189
    94764#, php-format
    95765msgid "n = %s"
    96766msgstr ""
    97767
    98 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:104
    99 #: src/PHP/Admin/AdminMenu.php:104
     768#: src/PHP/Admin/AdminMenu.php:191
    100769msgid "Cost and estimated comparison"
    101770msgstr ""
    102771
    103 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:105
    104 #: src/PHP/Admin/AdminMenu.php:105
     772#: src/PHP/Admin/AdminMenu.php:192
    105773msgid "AI cost recorded"
    106774msgstr ""
    107775
    108776#. translators: 1: cost from provider usage, 2: estimated cost
    109 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:107
    110 #: src/PHP/Admin/AdminMenu.php:107
     777#: src/PHP/Admin/AdminMenu.php:194
    111778#, php-format
    112779msgid "%1$s from provider usage, %2$s estimated"
    … …  
    114781
    115782#. translators: %s: number of AI requests
    116 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:109
    117 #: src/PHP/Admin/AdminMenu.php:109
     783#: src/PHP/Admin/AdminMenu.php:196
    118784#, php-format
    119785msgid "%s requests used a model with no known price and are counted as $0."
    120786msgstr ""
    121787
    122 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:110
    123 #: src/PHP/Admin/AdminMenu.php:110
     788#: src/PHP/Admin/AdminMenu.php:197
    124789msgid "Plugin cost, allocated per day"
    125790msgstr ""
    126791
    127 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:111
    128 #: src/PHP/Admin/AdminMenu.php:111
     792#: src/PHP/Admin/AdminMenu.php:198
    129793msgid "Other costs you entered"
    130794msgstr ""
    131795
    132 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:112
    133 #: src/PHP/Admin/AdminMenu.php:112
     796#: src/PHP/Admin/AdminMenu.php:199
    134797msgid "Total"
    135798msgstr ""
    136799
    137800#. translators: %s: name of the reference option, e.g. "SaaS AI agent"
    138 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:114
    139 #: src/PHP/Admin/AdminMenu.php:114
     801#: src/PHP/Admin/AdminMenu.php:201
    140802#, php-format
    141803msgid "Reference option: %s"
    … …  
    143805
    144806#. translators: 1: volume, 2: unit price
    145 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:116
    146 #: src/PHP/Admin/AdminMenu.php:116
     807#: src/PHP/Admin/AdminMenu.php:203
    147808#, php-format
    148809msgid "%1$s × %2$s"
    149810msgstr ""
    150811
    151 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:117
    152 #: src/PHP/Admin/AdminMenu.php:117
     812#: src/PHP/Admin/AdminMenu.php:204
    153813msgid "Estimated difference"
    154814msgstr ""
    155815
    156 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:118
    157 #: src/PHP/Admin/AdminMenu.php:118
     816#: src/PHP/Admin/AdminMenu.php:205
    158817msgid "Estimate based on the reference option you chose; not a verified saving."
    159818msgstr ""
    160819
    161 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:119
    162 #: src/PHP/Admin/AdminMenu.php:119
     820#: src/PHP/Admin/AdminMenu.php:206
    163821msgid "Add a reference option in Measurement settings to compare costs."
    164822msgstr ""
    165823
    166 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:120
    167 #: src/PHP/Admin/AdminMenu.php:120
     824#: src/PHP/Admin/AdminMenu.php:207
    168825msgid "AI cost covers model usage only. It is not the total cost of ownership."
    169826msgstr ""
    170827
    171 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:121
    172 #: src/PHP/Admin/AdminMenu.php:121
     828#: src/PHP/Admin/AdminMenu.php:208
    173829msgid "per AI conversation"
    174830msgstr ""
    175831
    176 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:122
    177 #: src/PHP/Admin/AdminMenu.php:122
     832#: src/PHP/Admin/AdminMenu.php:209
    178833msgid "per conversation the customer confirmed as resolved"
    179834msgstr ""
    180835
    181 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:123
    182 #: src/PHP/Admin/AdminMenu.php:123
     836#: src/PHP/Admin/AdminMenu.php:210
    183837msgid "per month"
    184838msgstr ""
    185839
    186840#. translators: %s: source of the reference price
    187 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:125
    188 #: src/PHP/Admin/AdminMenu.php:125
     841#: src/PHP/Admin/AdminMenu.php:212
    189842#, php-format
    190843msgid "Source: %s"
    191844msgstr ""
    192845
    193 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:127
    194 #: src/PHP/Admin/AdminMenu.php:127
     846#: src/PHP/Admin/AdminMenu.php:214
    195847msgid "AI-assisted orders"
    196848msgstr ""
    197849
    198 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:128
    199 #: premium/CRM/CrmStore.php:639
    200 #: src/PHP/Admin/AdminMenu.php:128
    201 msgid "Orders"
    202 msgstr ""
    203 
    204 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:129
    205 #: src/PHP/Admin/AdminMenu.php:129
     850#: src/PHP/Admin/AdminMenu.php:216
    206851msgid "AI-assisted does not mean the AI was the only reason for the order."
    207852msgstr ""
    208853
    209854#. translators: 1: attribution window such as "24 hours", 2: list of order statuses
    210 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:131
    211 #: src/PHP/Admin/AdminMenu.php:131
     855#: src/PHP/Admin/AdminMenu.php:218
    212856#, php-format
    213857msgid "Orders placed within %1$s after a qualifying chat interaction: a reply with products from your store, or a product or cart link clicked in the chat. Counted statuses: %2$s."
    214858msgstr ""
    215859
    216 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:132
    217 #: src/PHP/Admin/AdminMenu.php:132
     860#: src/PHP/Admin/AdminMenu.php:219
    218861msgid "AI-assisted revenue"
    219862msgstr ""
    220863
    221 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:133
    222 #: src/PHP/Admin/AdminMenu.php:133
     864#: src/PHP/Admin/AdminMenu.php:220
    223865msgid "Goods value after discounts, excluding tax and shipping. Partial refunds are deducted; fully refunded, cancelled and failed orders are excluded. Shown per currency, never converted."
    224866msgstr ""
    225867
    226 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:134
    227 #: src/PHP/Admin/AdminMenu.php:134
     868#: src/PHP/Admin/AdminMenu.php:221
    228869msgid "Revenue per currency is included in the Starter and Pro plans."
    229870msgstr ""
    230871
    231 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:135
    232 #: src/PHP/Admin/AdminMenu.php:135
     872#: src/PHP/Admin/AdminMenu.php:222
    233873msgid "No AI-assisted revenue in this period."
    234874msgstr ""
    235875
    236 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:137
    237 #: src/PHP/Admin/AdminMenu.php:137
     876#: src/PHP/Admin/AdminMenu.php:224
    238877msgid "Conversation outcomes"
    239878msgstr ""
    240879
    241 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:138
    242 #: src/PHP/Admin/AdminMenu.php:138
     880#: src/PHP/Admin/AdminMenu.php:225
    243881msgid "Finished conversations"
    244882msgstr ""
    245883
    246884#. translators: 1: finished conversations so far, 2: number needed
    247 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:140
    248 #: src/PHP/Admin/AdminMenu.php:140
     885#: src/PHP/Admin/AdminMenu.php:227
    249886#, php-format
    250887msgid "Not enough data yet: %1$s of %2$s finished conversations needed before rates are shown."
    251888msgstr ""
    252889
    253 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:141
    254 #: src/PHP/Admin/AdminMenu.php:141
     890#: src/PHP/Admin/AdminMenu.php:228
    255891msgid "Resolved, confirmed by the customer"
    256892msgstr ""
    257893
    258 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:142
    259 #: src/PHP/Admin/AdminMenu.php:142
     894#: src/PHP/Admin/AdminMenu.php:229
    260895msgid "Probably resolved: the customer clicked a product, added to cart or ordered"
    261896msgstr ""
    262897
    263 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:143
    264 #: src/PHP/Admin/AdminMenu.php:143
     898#: src/PHP/Admin/AdminMenu.php:230
    265899msgid "Handed off to staff"
    266900msgstr ""
    267901
    268 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:144
    269 #: src/PHP/Admin/AdminMenu.php:144
     902#: src/PHP/Admin/AdminMenu.php:231
    270903msgid "Unresolved, left before an answer, or unknown"
    271904msgstr ""
    272905
    273906#. translators: 1: lower bound, 2: upper bound of a 95% confidence interval, in percent
    274 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:146
    275 #: src/PHP/Admin/AdminMenu.php:146
     907#: src/PHP/Admin/AdminMenu.php:233
    276908#, php-format
    277909msgid "95%% range: %1$s–%2$s%%"
    278910msgstr ""
    279911
    280 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:147
    281 #: src/PHP/Admin/AdminMenu.php:147
     912#: src/PHP/Admin/AdminMenu.php:234
    282913msgid "\"Not handed off\" does not mean \"resolved\". A conversation is finished after 30 minutes without a new message; left-before-an-answer and unknown are never counted as success."
    283914msgstr ""
    284915
    285 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:148
    286 #: src/PHP/Admin/AdminMenu.php:148
     916#: src/PHP/Admin/AdminMenu.php:235
    287917msgid "Handoffs by source"
    288918msgstr ""
    289919
    290 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:149
    291 #: src/PHP/Admin/AdminMenu.php:149
     920#: src/PHP/Admin/AdminMenu.php:236
    292921msgid "AI"
    293922msgstr ""
    294923
    295 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:150
    296 #: src/PHP/Admin/AdminMenu.php:150
     924#: src/PHP/Admin/AdminMenu.php:237
    297925msgid "Automation rule"
    298926msgstr ""
    299927
    300 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:151
    301 #: src/PHP/Admin/AdminMenu.php:151
     928#: src/PHP/Admin/AdminMenu.php:238
    302929msgid "Customer button"
    303930msgstr ""
    304931
    305 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:152
    306 #: src/PHP/Admin/AdminMenu.php:152
     932#: src/PHP/Admin/AdminMenu.php:239
    307933msgid "Staff took over"
    308934msgstr ""
    309935
    310 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:153
    311 #: src/PHP/Admin/AdminMenu.php:153
     936#: src/PHP/Admin/AdminMenu.php:240
    312937msgid "AI budget reached"
    313938msgstr ""
    314939
    315 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:155
    316 #: src/PHP/Admin/AdminMenu.php:155
     940#: src/PHP/Admin/AdminMenu.php:242
    317941msgid "Measurement settings"
    318942msgstr ""
    319943
    320 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:156
    321 #: src/PHP/Admin/AdminMenu.php:156
     944#: src/PHP/Admin/AdminMenu.php:243
    322945msgid "Reference option name"
    323946msgstr ""
    324947
    325 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:157
    326 #: src/PHP/Admin/AdminMenu.php:157
     948#: src/PHP/Admin/AdminMenu.php:244
    327949msgid "Pricing unit"
    328950msgstr ""
    329951
    330 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:158
    331 #: src/PHP/Admin/AdminMenu.php:158
     952#: src/PHP/Admin/AdminMenu.php:245
    332953msgid "Price (USD)"
    333954msgstr ""
    334955
    335 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:159
    336 #: src/PHP/Admin/AdminMenu.php:159
     956#: src/PHP/Admin/AdminMenu.php:246
    337957msgid "Where the price comes from"
    338958msgstr ""
    339959
    340 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:160
    341 #: src/PHP/Admin/AdminMenu.php:160
     960#: src/PHP/Admin/AdminMenu.php:247
    342961msgid "Price date"
    343962msgstr ""
    344963
    345 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:161
    346 #: src/PHP/Admin/AdminMenu.php:161
     964#: src/PHP/Admin/AdminMenu.php:248
    347965msgid "Enter the price of an alternative you are considering. Leave the price at 0 to show costs only."
    348966msgstr ""
    349967
    350 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:162
    351 #: src/PHP/Admin/AdminMenu.php:162
     968#: src/PHP/Admin/AdminMenu.php:249
    352969msgid "Plugin cost per month (USD)"
    353970msgstr ""
    354971
    355 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:163
    356 #: src/PHP/Admin/AdminMenu.php:163
     972#: src/PHP/Admin/AdminMenu.php:250
    357973msgid "Other costs per month (USD)"
    358974msgstr ""
    359975
    360 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:164
    361 #: src/PHP/Admin/AdminMenu.php:164
     976#: src/PHP/Admin/AdminMenu.php:251
    362977msgid "Attribution window"
    363978msgstr ""
    364979
    365 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:165
    366 #: src/PHP/Admin/AdminMenu.php:165
     980#: src/PHP/Admin/AdminMenu.php:252
    367981msgid "How long after a qualifying chat interaction an order still counts as AI-assisted."
    368982msgstr ""
    369983
    370 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:166
    371 #: src/PHP/Admin/AdminMenu.php:166
     984#: src/PHP/Admin/AdminMenu.php:253
    372985msgid "24 hours"
    373986msgstr ""
    374987
    375 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:167
    376 #: src/PHP/Admin/AdminMenu.php:167
     988#: src/PHP/Admin/AdminMenu.php:254
    377989msgid "72 hours"
    378990msgstr ""
    379991
    380 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:168
    381 #: src/PHP/Admin/AdminMenu.php:168
     992#: src/PHP/Admin/AdminMenu.php:255
    382993msgid "7 days"
    383994msgstr ""
    384995
    385 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:169
    386 #: src/PHP/Admin/AdminMenu.php:169
     996#: src/PHP/Admin/AdminMenu.php:256
    387997msgid "Order statuses that count"
    388998msgstr ""
    389999
    390 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:170
    391 #: src/PHP/Admin/AdminMenu.php:170
     1000#: src/PHP/Admin/AdminMenu.php:257
    3921001msgid "Processing"
    3931002msgstr ""
    3941003
    395 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:171
    396 #: src/PHP/Admin/AdminMenu.php:171
     1004#: src/PHP/Admin/AdminMenu.php:258
    3971005msgid "Completed"
    3981006msgstr ""
    3991007
    400 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:172
    401 #: src/PHP/Admin/AdminMenu.php:172
     1008#: src/PHP/Admin/AdminMenu.php:259
    4021009msgid "On hold"
    4031010msgstr ""
    4041011
    405 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:173
    406 #: src/PHP/Admin/AdminMenu.php:173
     1012#: src/PHP/Admin/AdminMenu.php:260
    4071013msgid "Save settings"
    4081014msgstr ""
    4091015
    410 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:174
    411 #: src/PHP/Admin/AdminMenu.php:174
     1016#: src/PHP/Admin/AdminMenu.php:261
    4121017msgid "Saving…"
    4131018msgstr ""
    4141019
    415 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:175
    416 #: src/PHP/Admin/AdminMenu.php:175
     1020#: src/PHP/Admin/AdminMenu.php:262
    4171021msgid "Measurement settings saved."
    4181022msgstr ""
    4191023
    420 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:176
    421 #: src/PHP/Admin/AdminMenu.php:176
     1024#: src/PHP/Admin/AdminMenu.php:263
    4221025msgid "Could not save the measurement settings."
    4231026msgstr ""
    4241027
    425 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:94
     1028#. translators: %s: number of shopper conversations the AI assistant answered in the last 90 days.
     1029#: src/PHP/Admin/ReviewPrompt.php:175
     1030#, php-format
     1031msgid "In the last 90 days, your AI sales assistant answered %s shopper conversation."
     1032msgid_plural "In the last 90 days, your AI sales assistant answered %s shopper conversations."
     1033msgstr[0] ""
     1034msgstr[1] ""
     1035
     1036#. translators: %s: number of orders placed after a chat with the AI assistant.
     1037#: src/PHP/Admin/ReviewPrompt.php:181
     1038#, php-format
     1039msgid "%s order was placed after a chat with it."
     1040msgid_plural "%s orders were placed after a chat with it."
     1041msgstr[0] ""
     1042msgstr[1] ""
     1043
     1044#: src/PHP/Admin/ReviewPrompt.php:192
     1045msgid "If it has been useful, would you share your honest experience in a review on WordPress.org? It helps other store owners decide whether it fits their shop."
     1046msgstr ""
     1047
     1048#: src/PHP/Admin/ReviewPrompt.php:194
     1049msgid "Rate on WordPress.org"
     1050msgstr ""
     1051
     1052#: src/PHP/Admin/ReviewPrompt.php:196
     1053msgid "Maybe later"
     1054msgstr ""
     1055
     1056#: src/PHP/Admin/ReviewPrompt.php:198
     1057msgid "Don't ask again"
     1058msgstr ""
     1059
     1060#: src/PHP/Admin/ReviewPrompt.php:200
     1061msgid "Need help? Ask on the support forum"
     1062msgstr ""
     1063
    4261064#: src/PHP/Admin/SecurityNotice.php:94
    4271065msgid "Ninhle AI Sales Assistant: rotate your API credentials"
    4281066msgstr ""
    4291067
    430 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:95
    4311068#: src/PHP/Admin/SecurityNotice.php:95
    4321069msgid "This site previously ran a version of Ninhle AI Sales Assistant that stored provider credentials in plain text in the database. Those values have now been encrypted, but they should be treated as already exposed — anything written in plain text also went into every database backup taken since."
    4331070msgstr ""
    4341071
    435 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:99
    4361072#: src/PHP/Admin/SecurityNotice.php:99
    4371073msgid "Create new credentials at each provider you use (OpenAI, Anthropic, Google Gemini, Telegram bot), revoke the old ones, and enter the new values in Ninhle AI Sales Assistant settings."
    4381074msgstr ""
    4391075
    440 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:104
    4411076#: src/PHP/Admin/SecurityNotice.php:104
    4421077msgid "Dismiss this notice"
    4431078msgstr ""
    4441079
    445 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:16
    446 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:17
    4471080#: src/PHP/Admin/SettingsPage.php:16
    4481081#: src/PHP/Admin/SettingsPage.php:17
    … …  
    4511084
    4521085#. translators: %s: commission rate, for example "30%"
    453 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:45
    4541086#: src/PHP/Admin/SettingsPage.php:45
    4551087#, php-format
    … …  
    4571089msgstr ""
    4581090
    459 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:68
    4601091#: src/PHP/AI/AIService.php:68
    4611092msgid ""
    … …  
    4651096msgstr ""
    4661097
    467 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:232
    468 #: src/PHP/AI/AIService.php:232
     1098#: src/PHP/AI/AIService.php:258
    4691099msgid "The assistant is running in local-only mode. Please contact us through the channels below for help."
    4701100msgstr ""
    4711101
    472 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:250
    473 #: src/PHP/AI/AIService.php:250
     1102#: src/PHP/AI/AIService.php:277
    4741103msgid "Hello! The AI assistant is still being set up. Please try again shortly, or contact us through the channels below."
    4751104msgstr ""
    4761105
    477 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:304
    478 #: src/PHP/AI/AIService.php:304
     1106#: src/PHP/AI/AIService.php:331
    4791107msgid "Sorry, the assistant is temporarily unavailable. Please try again in a moment, or choose \"Talk to a person\"."
    4801108msgstr ""
    4811109
    482 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:400
    483 #: src/PHP/AI/AIService.php:400
    484 msgid "I am still looking this up for you, one moment please."
    485 msgstr ""
    486 
    487 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:406
    488 #: src/PHP/AI/AIService.php:406
     1110#: src/PHP/AI/AIService.php:455
     1111msgid "Sorry, I could not finish looking that up. Please ask me again in a moment."
     1112msgstr ""
     1113
     1114#: src/PHP/AI/AIService.php:461
    4891115msgid "Sorry, I could not put an answer together just now. Could you ask that again in a different way?"
    4901116msgstr ""
    4911117
    492 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:417
    493 #: src/PHP/AI/AIService.php:417
     1118#: src/PHP/AI/AIService.php:472
    4941119msgid "I understand. Let me bring in a member of our team to help you directly. Please wait a moment."
    4951120msgstr ""
    4961121
    497 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:418
    498 #: src/PHP/AI/AIService.php:418
     1122#: src/PHP/AI/AIService.php:473
    4991123msgid "We are connecting you with a manager now. Please stay in the chat."
    5001124msgstr ""
    5011125
    502 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:419
    503 #: src/PHP/AI/AIService.php:419
     1126#: src/PHP/AI/AIService.php:474
    5041127msgid "Your request is being escalated urgently to the responsible team. Please do not send any more personal information here."
    5051128msgstr ""
    5061129
    507 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:69
    5081130#: src/PHP/Commerce/LocalHandler.php:69
    5091131msgid "Hello! How can I help you today?"
    … …  
    5111133
    5121134#. translators: %s is the store or brand name.
    513 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:74
    5141135#: src/PHP/Commerce/LocalHandler.php:74
    5151136#, php-format
    … …  
    5171138msgstr ""
    5181139
    519 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:80
    5201140#: src/PHP/Commerce/LocalHandler.php:80
    5211141msgid "You are welcome. Just message me if you need anything else."
    5221142msgstr ""
    5231143
    524 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:123
    5251144#: src/PHP/Commerce/LocalHandler.php:123
    5261145msgid "Your cart is empty. Tell me which product you want and I will send you the link."
    … …  
    5281147
    5291148#. translators: %s is the checkout page URL.
    530 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:135
    5311149#: src/PHP/Commerce/LocalHandler.php:135
    5321150#, php-format
    … …  
    5351153
    5361154#. translators: %s is the cart page URL.
    537 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:155
    5381155#: src/PHP/Commerce/LocalHandler.php:155
    5391156#, php-format
    … …  
    5411158msgstr ""
    5421159
    543 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:163
    5441160#: src/PHP/Commerce/LocalHandler.php:163
     1161#: src/PHP/Widget/WidgetLoader.php:142
    5451162msgid "Your cart is empty."
    5461163msgstr ""
    5471164
    5481165#. translators: 1: list of cart lines, one per line. 2: checkout URL.
    549 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:173
    5501166#: src/PHP/Commerce/LocalHandler.php:173
    5511167#, php-format
    … …  
    5581174
    5591175#. translators: 1: order number. 2: order status. 3: date the order was placed.
    560 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:214
    561 #: src/PHP/Commerce/LocalHandler.php:214
     1176#: src/PHP/Commerce/LocalHandler.php:218
    5621177#, php-format
    5631178msgid "Order %1$s is currently: %2$s (placed on %3$s)."
    5641179msgstr ""
    5651180
    566 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:197
    5671181#: src/PHP/Commerce/Woo/LeadService.php:197
    5681182msgid "Human support needed"
    5691183msgstr ""
    5701184
    571 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:198
    5721185#: src/PHP/Commerce/Woo/LeadService.php:198
    5731186msgid "Priority:"
    5741187msgstr ""
    5751188
    576 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:199
    5771189#: src/PHP/Commerce/Woo/LeadService.php:199
    5781190msgid "Issue:"
    5791191msgstr ""
    5801192
    581 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:200
    5821193#: src/PHP/Commerce/Woo/LeadService.php:200
    5831194msgid "Summary:"
    … …  
    5851196
    5861197#. translators: %d: conversation ID.
    587 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:202
    5881198#: src/PHP/Commerce/Woo/LeadService.php:202
    5891199#, php-format
    … …  
    5911201msgstr ""
    5921202
    593 #: build/ninhle-ai-sales-assistant/src/PHP/Core/JobDispatcher.php:69
    594 #: premium/REST/TelegramController.php:55
    595 #: src/PHP/Core/JobDispatcher.php:69
    596 msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly."
    597 msgstr ""
    598 
    599 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:35
    600 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:43
     1203#: src/PHP/Core/AsyncKick.php:342
     1204msgid "Ninhle AI Sales Assistant: background replies are blocked by your host"
     1205msgstr ""
     1206
     1207#: src/PHP/Core/AsyncKick.php:343
     1208msgid "The chat still works, but each AI reply now runs while the customer's chat window waits, which keeps a server worker busy for several seconds per reply."
     1209msgstr ""
     1210
     1211#: src/PHP/Core/AsyncKick.php:344
     1212msgid "Ask your host to allow WordPress loopback requests (Tools > Site Health shows the same check) and to let PHP finish requests in the background. The plugin checks again once a day and when you save settings."
     1213msgstr ""
     1214
    6011215#: src/PHP/Privacy/PersonalData.php:35
    6021216#: src/PHP/Privacy/PersonalData.php:43
    … …  
    6041218msgstr ""
    6051219
    606 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:144
    607 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:206
    6081220#: src/PHP/Privacy/PersonalData.php:144
    609 #: src/PHP/Privacy/PersonalData.php:206
     1221#: src/PHP/Privacy/PersonalData.php:229
    6101222msgid "Conversation"
    6111223msgstr ""
    6121224
    613 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:145
    6141225#: src/PHP/Privacy/PersonalData.php:145
    6151226msgid "Sender"
    6161227msgstr ""
    6171228
    618 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:146
    6191229#: src/PHP/Privacy/PersonalData.php:146
    6201230msgid "Message"
    6211231msgstr ""
    6221232
    623 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:147
    6241233#: src/PHP/Privacy/PersonalData.php:147
    6251234msgid "Sent"
    6261235msgstr ""
    6271236
    628 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:153
    6291237#: src/PHP/Privacy/PersonalData.php:153
    6301238msgid "Sender first name"
    6311239msgstr ""
    6321240
    633 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:154
    6341241#: src/PHP/Privacy/PersonalData.php:154
    6351242msgid "Sender last name"
    6361243msgstr ""
    6371244
    638 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:155
    6391245#: src/PHP/Privacy/PersonalData.php:155
    6401246msgid "Sender avatar"
    6411247msgstr ""
    6421248
    643 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:164
    6441249#: src/PHP/Privacy/PersonalData.php:164
    6451250msgid "Ninhle AI Sales Assistant chat messages"
    6461251msgstr ""
    6471252
    648 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:203
    649 #: src/PHP/Privacy/PersonalData.php:203
     1253#: src/PHP/Privacy/PersonalData.php:205
     1254msgid "Order linked to this chat"
     1255msgstr ""
     1256
     1257#: src/PHP/Privacy/PersonalData.php:208
     1258msgid "Question the assistant could not answer (contact details hidden)"
     1259msgstr ""
     1260
     1261#: src/PHP/Privacy/PersonalData.php:211
     1262msgid "Customer identified in this chat"
     1263msgstr ""
     1264
     1265#. translators: %d: WordPress user ID of the logged-in customer.
     1266#: src/PHP/Privacy/PersonalData.php:213
     1267#, php-format
     1268msgid "Logged-in account #%d"
     1269msgstr ""
     1270
     1271#. translators: %d: WooCommerce order ID whose ownership the customer proved in the chat.
     1272#: src/PHP/Privacy/PersonalData.php:215
     1273#, php-format
     1274msgid "Order ID %d (ownership checked)"
     1275msgstr ""
     1276
     1277#: src/PHP/Privacy/PersonalData.php:218
     1278msgid "Reason the chat did not lead to an order (AI label)"
     1279msgstr ""
     1280
     1281#: src/PHP/Privacy/PersonalData.php:222
     1282msgid "Answer to \"Did this answer your question?\""
     1283msgstr ""
     1284
     1285#: src/PHP/Privacy/PersonalData.php:226
    6501286msgid "Ninhle AI Sales Assistant chat statistics"
    6511287msgstr ""
    6521288
    653 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:208
    654 #: src/PHP/Privacy/PersonalData.php:208
    655 msgid "Order linked to this chat"
    656 msgstr ""
    657 
    658 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:209
    659 #: src/PHP/Privacy/PersonalData.php:209
    660 msgid "Answer to \"Did this answer your question?\""
    661 msgstr ""
    662 
    663 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:210
    664 #: src/PHP/Privacy/PersonalData.php:210
     1289#: src/PHP/Privacy/PersonalData.php:231
    6651290msgid "Recorded (UTC)"
    6661291msgstr ""
    6671292
    668 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:228
    669 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:145
    670 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:89
    671 #: premium/CRM/CrmStore.php:610
    672 #: src/PHP/Privacy/PersonalData.php:228
    673 #: src/PHP/REST/LeadController.php:145
    674 #: src/PHP/Widget/WidgetLoader.php:89
    675 msgid "Name"
    676 msgstr ""
    677 
    678 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:229
    679 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:146
    680 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:90
    681 #: src/PHP/Privacy/PersonalData.php:229
     1293#: src/PHP/Privacy/PersonalData.php:250
    6821294#: src/PHP/REST/LeadController.php:146
    683 #: src/PHP/Widget/WidgetLoader.php:90
     1295#: src/PHP/Widget/WidgetLoader.php:95
    6841296msgid "Email"
    6851297msgstr ""
    6861298
    687 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:230
    688 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:147
    689 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:91
    690 #: src/PHP/Privacy/PersonalData.php:230
     1299#: src/PHP/Privacy/PersonalData.php:251
    6911300#: src/PHP/REST/LeadController.php:147
    692 #: src/PHP/Widget/WidgetLoader.php:91
     1301#: src/PHP/Widget/WidgetLoader.php:96
    6931302msgid "Phone"
    6941303msgstr ""
    6951304
    696 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:231
    697 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:148
    698 #: premium/CRM/CrmStore.php:626
    699 #: premium/CRM/CrmStore.php:676
    700 #: src/PHP/Privacy/PersonalData.php:231
    701 #: src/PHP/REST/LeadController.php:148
    702 msgid "Status"
    703 msgstr ""
    704 
    705 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:232
    706 #: src/PHP/Privacy/PersonalData.php:232
     1305#: src/PHP/Privacy/PersonalData.php:253
    7071306msgid "Lead score"
    7081307msgstr ""
    7091308
    710 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:233
    711 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:150
    712 #: premium/CRM/CrmStore.php:640
    713 #: premium/CRM/CrmStore.php:678
    714 #: src/PHP/Privacy/PersonalData.php:233
    715 #: src/PHP/REST/LeadController.php:150
    716 msgid "Created"
    717 msgstr ""
    718 
    719 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:240
    720 #: src/PHP/Privacy/PersonalData.php:240
     1309#: src/PHP/Privacy/PersonalData.php:261
    7211310msgid "Additional fields"
    7221311msgstr ""
    7231312
    724 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:247
    725 #: src/PHP/Privacy/PersonalData.php:247
     1313#: src/PHP/Privacy/PersonalData.php:268
    7261314msgid "Ninhle AI Sales Assistant contact details"
    7271315msgstr ""
    7281316
    729 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:272
    730 #: src/PHP/Privacy/PersonalData.php:272
     1317#: src/PHP/Privacy/PersonalData.php:293
    7311318msgid "Ninhle AI Sales Assistant chat sessions"
    7321319msgstr ""
    7331320
    734 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:275
    735 #: premium/CRM/CrmStore.php:625
    736 #: premium/CRM/CrmStore.php:666
    737 #: src/PHP/Privacy/PersonalData.php:275
    738 msgid "Channel"
    739 msgstr ""
    740 
    741 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:276
    742 #: src/PHP/Privacy/PersonalData.php:276
     1321#: src/PHP/Privacy/PersonalData.php:297
    7431322msgid "Session reference"
    7441323msgstr ""
    7451324
    746 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:277
    747 #: src/PHP/Privacy/PersonalData.php:277
     1325#: src/PHP/Privacy/PersonalData.php:298
    7481326msgid "First seen"
    7491327msgstr ""
    7501328
    751 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:310
    752 #: src/PHP/Privacy/PersonalData.php:310
     1329#: src/PHP/Privacy/PersonalData.php:331
    7531330msgid "Ninhle AI Sales Assistant activity log"
    7541331msgstr ""
    7551332
    756 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:313
    757 #: src/PHP/Privacy/PersonalData.php:313
     1333#: src/PHP/Privacy/PersonalData.php:334
    7581334msgid "Action"
    7591335msgstr ""
    7601336
    761 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:314
    762 #: src/PHP/Privacy/PersonalData.php:314
     1337#: src/PHP/Privacy/PersonalData.php:335
    7631338msgid "IP address"
    7641339msgstr ""
    7651340
    766 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:315
    767 #: src/PHP/Privacy/PersonalData.php:315
     1341#: src/PHP/Privacy/PersonalData.php:336
    7681342msgid "Browser"
    7691343msgstr ""
    7701344
    771 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:316
    772 #: premium/CRM/CrmStore.php:668
    773 #: src/PHP/Privacy/PersonalData.php:316
    774 msgid "When"
    775 msgstr ""
    776 
    777 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:409
    778 #: src/PHP/Privacy/PersonalData.php:409
     1345#: src/PHP/Privacy/PersonalData.php:430
    7791346msgid "Conversation text that was previously sent to an enabled third-party AI provider is subject to that provider's own retention policy and cannot be deleted from here."
    7801347msgstr ""
    7811348
    782 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:530
    783 #: src/PHP/Privacy/PersonalData.php:530
     1349#: src/PHP/Privacy/PersonalData.php:551
    7841350msgid "This person was linked to records in a connected CRM. Copies already synced there must be erased in that CRM."
    7851351msgstr ""
    7861352
    787 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:538
    788 #: src/PHP/Privacy/PersonalData.php:538
     1353#: src/PHP/Privacy/PersonalData.php:559
    7891354msgid "Customer records in Ninhle AI Sales Assistant CRM could not be erased because of a database error. Please run the erasure again."
    7901355msgstr ""
    7911356
    792 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:42
    7931357#: src/PHP/Privacy/PolicyContent.php:42
    7941358msgid "This site uses Ninhle AI Sales Assistant, an AI sales assistant. When you use the chat widget, the following information is stored in this site's own database: the messages you send and receive, the page you started the conversation on, and any name, email address or phone number you choose to provide."
    7951359msgstr ""
    7961360
    797 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:46
    7981361#: src/PHP/Privacy/PolicyContent.php:46
    7991362msgid "Your IP address and browser identification are recorded alongside each message, so the store can investigate abuse of the chat. This information is deleted together with your conversation."
    8001363msgstr ""
    8011364
    802 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:49
    8031365#: src/PHP/Privacy/PolicyContent.php:49
    8041366msgid "The chat widget stores two kinds of item in your browser's local storage: a session token that lets you return to your conversation, and a note that the \"Did this answer your question?\" prompt was already shown, so it is not asked twice. They are not cookies, they are not used for tracking, and they are not shared with any other site. Clearing your browser data removes them."
    8051367msgstr ""
    8061368
    807 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:53
    8081369#: src/PHP/Privacy/PolicyContent.php:53
    8091370msgid "When the assistant shows you products from the store, or you click a product or cart link in the chat, the conversation number is saved in your shopping session (the store's WooCommerce session cookie). If you then place an order within the store's attribution window, the order is linked to that conversation so the store can count orders the assistant helped with. The link is removed when your conversation data is deleted; the order itself is kept as usual."
    8101371msgstr ""
    8111372
    812 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:55
    8131373#: src/PHP/Privacy/PolicyContent.php:55
    8141374msgid "If you answer \"Did this answer your question?\" in the chat, your answer is stored with the conversation and used only for the store's own statistics."
    … …  
    8161376
    8171377#. translators: %s is the name of the configured AI provider.
    818 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:59
    8191378#: src/PHP/Privacy/PolicyContent.php:59
    8201379#, php-format
    … …  
    8221381msgstr ""
    8231382
    824 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:64
    8251383#: src/PHP/Privacy/PolicyContent.php:64
    8261384msgid "When you leave your contact details, or when a conversation is passed to a human, a notification containing those details is sent to the store team over Telegram."
    8271385msgstr ""
    8281386
    829 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:68
    8301387#: src/PHP/Privacy/PolicyContent.php:68
    8311388msgid "If you paste a web address into the chat, that address is sent to the third-party service r.jina.ai so its page content can be read."
    8321389msgstr ""
    8331390
    834 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:73
    8351391#: src/PHP/Privacy/PolicyContent.php:73
    8361392msgid "If you use the microphone button in the chat, speech recognition is done by your own web browser, which turns your speech into text in the message box. Depending on your browser, the audio may be sent to the browser maker's speech recognition service (for example Google for Chrome, Apple for Safari, or Microsoft for Edge). This site never receives or stores the audio; it only receives the text you choose to send."
    8371393msgstr ""
    8381394
    839 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:77
    8401395#: src/PHP/Privacy/PolicyContent.php:77
    8411396msgid "When you leave your contact details or request a human, an event containing those details is sent to an automation system configured by the store."
    … …  
    8431398
    8441399#. translators: %d is the number of days files are kept.
    845 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:84
    8461400#: src/PHP/Privacy/PolicyContent.php:84
    8471401#, php-format
    … …  
    8511405msgstr[1] ""
    8521406
    853 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:94
    8541407#: src/PHP/Privacy/PolicyContent.php:94
    8551408msgid "You can request a copy of this data, or ask for it to be deleted, using the contact details in this privacy policy. Conversations and contact details are not shared with anyone other than the services named above."
    8561409msgstr ""
    8571410
    858 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:99
    8591411#: src/PHP/Privacy/PolicyContent.php:99
    8601412msgid "A deletion request removes the personal data held by this site. Message text that was already sent to the services named above remains subject to those services' own retention policies and cannot be deleted from here."
    8611413msgstr ""
    8621414
    863 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:109
    8641415#: src/PHP/Privacy/PolicyContent.php:109
    8651416msgid "OpenAI (see openai.com/policies/privacy-policy)"
    8661417msgstr ""
    8671418
    868 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:110
    8691419#: src/PHP/Privacy/PolicyContent.php:110
    8701420msgid "Anthropic (see anthropic.com/legal/privacy)"
    8711421msgstr ""
    8721422
    873 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:111
    8741423#: src/PHP/Privacy/PolicyContent.php:111
    8751424msgid "Google (see policies.google.com/privacy)"
    8761425msgstr ""
    8771426
    878 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:114
    8791427#: src/PHP/Privacy/PolicyContent.php:114
    8801428msgid "the AI provider configured by the store owner"
    8811429msgstr ""
    8821430
    883 #: build/ninhle-ai-sales-assistant/src/PHP/REST/AdminController.php:253
    884 #: src/PHP/REST/AdminController.php:253
     1431#: src/PHP/REST/AdminController.php:293
    8851432msgid "The Telegram Chat ID is your bot's own ID. Enter the ID of the group or person that should receive notifications, then add the bot to that group (or send the bot a message first)."
    8861433msgstr ""
    8871434
    888 #: build/ninhle-ai-sales-assistant/src/PHP/REST/ChatController.php:24
    889 #: src/PHP/REST/ChatController.php:24
     1435#: src/PHP/REST/ChatController.php:27
    8901436msgid "Sales Assistant"
    8911437msgstr ""
    8921438
    893 #: build/ninhle-ai-sales-assistant/src/PHP/REST/ChatController.php:238
    894 #: src/PHP/REST/ChatController.php:238
     1439#: src/PHP/REST/ChatController.php:289
    8951440msgid "Thanks, our team has been notified. A member of staff will reply here as soon as possible."
    8961441msgstr ""
    8971442
    898 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:98
    899 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:142
    9001443#: src/PHP/REST/Guard.php:98
    9011444#: src/PHP/REST/Guard.php:142
    … …  
    9031446msgstr ""
    9041447
    905 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:124
    9061448#: src/PHP/REST/Guard.php:124
    9071449msgid "Security check failed. Please reload the page and try again."
    9081450msgstr ""
    9091451
    910 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:165
    9111452#: src/PHP/REST/Guard.php:165
    9121453msgid "Missing chat session."
    9131454msgstr ""
    9141455
    915 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:175
    9161456#: src/PHP/REST/Guard.php:175
    9171457msgid "Invalid or expired chat session."
    9181458msgstr ""
    9191459
    920 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:184
    921 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:199
    9221460#: src/PHP/REST/Guard.php:184
    9231461#: src/PHP/REST/Guard.php:199
    … …  
    9251463msgstr ""
    9261464
    927 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:215
    9281465#: src/PHP/REST/Guard.php:215
    9291466msgid "Too many requests. Please wait a moment."
    9301467msgstr ""
    9311468
    932 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:242
    9331469#: src/PHP/REST/Guard.php:242
    9341470msgid "Telegram webhook secret is not configured."
    9351471msgstr ""
    9361472
    937 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:255
    9381473#: src/PHP/REST/Guard.php:255
    9391474msgid "Invalid webhook signature."
    9401475msgstr ""
    9411476
    942 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:308
    9431477#: src/PHP/REST/Guard.php:308
    9441478msgid "Message must be text."
    9451479msgstr ""
    9461480
    947 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:313
    9481481#: src/PHP/REST/Guard.php:313
    9491482msgid "Message contains invalid characters."
    9501483msgstr ""
    9511484
    952 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:317
    9531485#: src/PHP/REST/Guard.php:317
    9541486msgid "Message cannot be empty."
    9551487msgstr ""
    9561488
    957 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:321
    9581489#: src/PHP/REST/Guard.php:321
    9591490msgid "Message is too large."
    9601491msgstr ""
    9611492
    962 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:325
    9631493#: src/PHP/REST/Guard.php:325
    9641494msgid "Message is too long."
    9651495msgstr ""
    9661496
    967 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:149
     1497#: src/PHP/REST/InsightsController.php:43
     1498msgid "Enter both the question and the answer."
     1499msgstr ""
     1500
     1501#: src/PHP/REST/InsightsController.php:66
     1502msgid "AI insights need the Starter plan."
     1503msgstr ""
     1504
    9681505#: src/PHP/REST/LeadController.php:149
    9691506msgid "Score"
    9701507msgstr ""
    9711508
    972 #: build/ninhle-ai-sales-assistant/src/PHP/Rules/RuleEngine.php:232
    9731509#: src/PHP/Rules/RuleEngine.php:232
    9741510msgid "A member of our team will join this chat shortly."
    9751511msgstr ""
    9761512
    977 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:68
    978 #: src/PHP/Widget/WidgetLoader.php:68
     1513#: src/PHP/Widget/WidgetLoader.php:71
    9791514msgid "Open chat"
    9801515msgstr ""
    9811516
    982 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:69
    983 #: src/PHP/Widget/WidgetLoader.php:69
     1517#. translators: %d: number of replies the shopper has not read yet.
     1518#: src/PHP/Widget/WidgetLoader.php:73
     1519#, php-format
     1520msgid "new replies: %d"
     1521msgstr ""
     1522
     1523#: src/PHP/Widget/WidgetLoader.php:74
    9841524msgid "Chat with our assistant"
    9851525msgstr ""
    9861526
    987 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:70
    988 #: src/PHP/Widget/WidgetLoader.php:70
     1527#: src/PHP/Widget/WidgetLoader.php:75
    9891528msgid "Cart"
    9901529msgstr ""
    9911530
    992 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:71
    993 #: src/PHP/Widget/WidgetLoader.php:71
     1531#: src/PHP/Widget/WidgetLoader.php:76
    9941532msgid "My details"
    9951533msgstr ""
    9961534
    997 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:72
    998 #: src/PHP/Widget/WidgetLoader.php:72
     1535#: src/PHP/Widget/WidgetLoader.php:77
    9991536msgid "Clear conversation"
    10001537msgstr ""
    10011538
    1002 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:73
    1003 #: src/PHP/Widget/WidgetLoader.php:73
     1539#: src/PHP/Widget/WidgetLoader.php:78
     1540#: src/PHP/Widget/WidgetLoader.php:139
    10041541msgid "Close"
    10051542msgstr ""
    10061543
    1007 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:74
    1008 #: src/PHP/Widget/WidgetLoader.php:74
     1544#: src/PHP/Widget/WidgetLoader.php:79
    10091545msgid "Clear the conversation history and start over?"
    10101546msgstr ""
    10111547
    1012 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:75
    1013 #: src/PHP/Widget/WidgetLoader.php:75
     1548#: src/PHP/Widget/WidgetLoader.php:80
    10141549msgid "Clear"
    10151550msgstr ""
    10161551
    1017 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:76
    1018 #: src/PHP/Widget/WidgetLoader.php:76
    1019 msgid "Cancel"
    1020 msgstr ""
    1021 
    1022 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:77
    1023 #: src/PHP/Widget/WidgetLoader.php:77
     1552#: src/PHP/Widget/WidgetLoader.php:82
    10241553msgid "Attach a photo or PDF"
    10251554msgstr ""
    10261555
    1027 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:78
    1028 #: src/PHP/Widget/WidgetLoader.php:78
     1556#: src/PHP/Widget/WidgetLoader.php:83
    10291557msgid "Uploading…"
    10301558msgstr ""
    10311559
    1032 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:79
    1033 #: src/PHP/Widget/WidgetLoader.php:79
     1560#: src/PHP/Widget/WidgetLoader.php:84
    10341561msgid "File upload failed."
    10351562msgstr ""
    10361563
    1037 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:80
    1038 #: src/PHP/Widget/WidgetLoader.php:80
     1564#: src/PHP/Widget/WidgetLoader.php:85
    10391565msgid "Your message could not be sent. Please try again."
    10401566msgstr ""
    10411567
    1042 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:81
    1043 #: src/PHP/Widget/WidgetLoader.php:81
     1568#: src/PHP/Widget/WidgetLoader.php:86
    10441569msgid "Call us"
    10451570msgstr ""
    10461571
    1047 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:82
    1048 #: src/PHP/Widget/WidgetLoader.php:82
     1572#: src/PHP/Widget/WidgetLoader.php:87
    10491573msgid "Send"
    10501574msgstr ""
    10511575
    1052 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:83
    1053 #: src/PHP/Widget/WidgetLoader.php:83
     1576#: src/PHP/Widget/WidgetLoader.php:88
    10541577msgid "Your message"
    10551578msgstr ""
    10561579
    1057 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:84
    1058 #: src/PHP/Widget/WidgetLoader.php:84
     1580#: src/PHP/Widget/WidgetLoader.php:89
    10591581msgid "Type your question…"
    10601582msgstr ""
    10611583
    10621584#. translators: %s is the maximum upload size in megabytes.
    1063 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:86
    1064 #: src/PHP/Widget/WidgetLoader.php:86
     1585#: src/PHP/Widget/WidgetLoader.php:91
    10651586#, php-format
    10661587msgid "File is too large. Please choose a file under %sMB."
    10671588msgstr ""
    10681589
    1069 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:87
    1070 #: src/PHP/Widget/WidgetLoader.php:87
     1590#: src/PHP/Widget/WidgetLoader.php:92
    10711591msgid "Thank you. Our team will contact you shortly."
    10721592msgstr ""
    10731593
    1074 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:88
    1075 #: src/PHP/Widget/WidgetLoader.php:88
     1594#: src/PHP/Widget/WidgetLoader.php:93
    10761595msgid "Your contact details"
    10771596msgstr ""
    10781597
    1079 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:92
    1080 #: src/PHP/Widget/WidgetLoader.php:92
     1598#: src/PHP/Widget/WidgetLoader.php:97
    10811599msgid "Submit"
    10821600msgstr ""
    10831601
    1084 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:93
    1085 #: src/PHP/Widget/WidgetLoader.php:93
     1602#: src/PHP/Widget/WidgetLoader.php:98
    10861603msgid "Submitting…"
    10871604msgstr ""
    10881605
    1089 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:94
    1090 #: src/PHP/Widget/WidgetLoader.php:94
     1606#: src/PHP/Widget/WidgetLoader.php:99
    10911607msgid "Please provide at least an email or phone number."
    10921608msgstr ""
    10931609
    1094 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:95
    1095 #: src/PHP/Widget/WidgetLoader.php:95
     1610#: src/PHP/Widget/WidgetLoader.php:100
    10961611msgid "Could not submit your details."
    10971612msgstr ""
    10981613
    1099 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:97
    1100 #: src/PHP/Widget/WidgetLoader.php:97
     1614#: src/PHP/Widget/WidgetLoader.php:102
    11011615msgid "More options"
    11021616msgstr ""
    11031617
    1104 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:98
    1105 #: src/PHP/Widget/WidgetLoader.php:98
     1618#: src/PHP/Widget/WidgetLoader.php:103
    11061619msgid "Expand"
    11071620msgstr ""
    11081621
    1109 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:99
    1110 #: src/PHP/Widget/WidgetLoader.php:99
     1622#: src/PHP/Widget/WidgetLoader.php:104
    11111623msgid "Make smaller"
    11121624msgstr ""
    11131625
    1114 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:100
    1115 #: src/PHP/Widget/WidgetLoader.php:100
     1626#: src/PHP/Widget/WidgetLoader.php:105
    11161627msgid "Talk to a person"
    11171628msgstr ""
    11181629
    1119 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:101
    1120 #: src/PHP/Widget/WidgetLoader.php:101
     1630#: src/PHP/Widget/WidgetLoader.php:106
    11211631msgid "AI assistant"
    11221632msgstr ""
    11231633
    1124 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:102
    1125 #: src/PHP/Widget/WidgetLoader.php:102
     1634#: src/PHP/Widget/WidgetLoader.php:107
    11261635msgid "Waiting for a team member"
    11271636msgstr ""
    11281637
    11291638#. translators: %s is the first name of the staff member answering the chat.
    1130 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:104
    1131 #: src/PHP/Widget/WidgetLoader.php:104
     1639#: src/PHP/Widget/WidgetLoader.php:109
    11321640#, php-format
    11331641msgid "%s is helping you"
    11341642msgstr ""
    11351643
    1136 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:105
    1137 #: src/PHP/Widget/WidgetLoader.php:105
     1644#: src/PHP/Widget/WidgetLoader.php:110
    11381645msgid "A team member is helping you"
    11391646msgstr ""
    11401647
    1141 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:106
    1142 #: src/PHP/Widget/WidgetLoader.php:106
     1648#: src/PHP/Widget/WidgetLoader.php:111
    11431649msgid "This conversation is closed"
    11441650msgstr ""
    11451651
    1146 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:107
    1147 #: src/PHP/Widget/WidgetLoader.php:107
     1652#: src/PHP/Widget/WidgetLoader.php:112
    11481653msgid "Writing a reply…"
    11491654msgstr ""
    11501655
    1151 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:108
    1152 #: src/PHP/Widget/WidgetLoader.php:108
     1656#: src/PHP/Widget/WidgetLoader.php:113
    11531657msgid "Checking product information…"
    11541658msgstr ""
    11551659
    1156 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:109
    1157 #: src/PHP/Widget/WidgetLoader.php:109
     1660#: src/PHP/Widget/WidgetLoader.php:114
    11581661msgid "Looking up your order…"
    11591662msgstr ""
    11601663
    1161 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:110
    1162 #: src/PHP/Widget/WidgetLoader.php:110
     1664#: src/PHP/Widget/WidgetLoader.php:115
    11631665msgid "Connecting you with a team member…"
    11641666msgstr ""
    11651667
    1166 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:111
    1167 #: src/PHP/Widget/WidgetLoader.php:111
     1668#: src/PHP/Widget/WidgetLoader.php:116
     1669msgid "Waiting in line…"
     1670msgstr ""
     1671
     1672#: src/PHP/Widget/WidgetLoader.php:119
     1673msgid "Previous products"
     1674msgstr ""
     1675
     1676#: src/PHP/Widget/WidgetLoader.php:120
     1677msgid "Next products"
     1678msgstr ""
     1679
     1680#: src/PHP/Widget/WidgetLoader.php:121
     1681msgid "View"
     1682msgstr ""
     1683
     1684#: src/PHP/Widget/WidgetLoader.php:122
     1685msgid "In stock"
     1686msgstr ""
     1687
     1688#: src/PHP/Widget/WidgetLoader.php:124
     1689msgid "On backorder"
     1690msgstr ""
     1691
     1692#. translators: %s: date the price was shown, e.g. "24 Oct 2026".
     1693#: src/PHP/Widget/WidgetLoader.php:126
     1694#, php-format
     1695msgid "Price on %s"
     1696msgstr ""
     1697
     1698#: src/PHP/Widget/WidgetLoader.php:128
     1699msgid "Add to cart"
     1700msgstr ""
     1701
     1702#: src/PHP/Widget/WidgetLoader.php:129
     1703msgid "Adding…"
     1704msgstr ""
     1705
     1706#: src/PHP/Widget/WidgetLoader.php:130
     1707msgid "Choose options"
     1708msgstr ""
     1709
     1710#: src/PHP/Widget/WidgetLoader.php:131
     1711msgid "Added to cart"
     1712msgstr ""
     1713
     1714#: src/PHP/Widget/WidgetLoader.php:132
     1715msgid "View cart"
     1716msgstr ""
     1717
     1718#: src/PHP/Widget/WidgetLoader.php:133
     1719msgid "Checkout"
     1720msgstr ""
     1721
     1722#: src/PHP/Widget/WidgetLoader.php:134
     1723msgid "Sorry, this item just sold out."
     1724msgstr ""
     1725
     1726#: src/PHP/Widget/WidgetLoader.php:135
     1727msgid "You can't add more of this item."
     1728msgstr ""
     1729
     1730#: src/PHP/Widget/WidgetLoader.php:136
     1731msgid "Couldn't add to cart."
     1732msgstr ""
     1733
     1734#: src/PHP/Widget/WidgetLoader.php:137
     1735msgid "Open the product page"
     1736msgstr ""
     1737
     1738#: src/PHP/Widget/WidgetLoader.php:138
     1739msgid "Your cart"
     1740msgstr ""
     1741
     1742#: src/PHP/Widget/WidgetLoader.php:140
     1743msgid "Loading your cart…"
     1744msgstr ""
     1745
     1746#: src/PHP/Widget/WidgetLoader.php:141
     1747msgid "Couldn't load your cart."
     1748msgstr ""
     1749
     1750#: src/PHP/Widget/WidgetLoader.php:143
     1751msgid "Subtotal"
     1752msgstr ""
     1753
     1754#: src/PHP/Widget/WidgetLoader.php:144
     1755msgid "Discount"
     1756msgstr ""
     1757
     1758#: src/PHP/Widget/WidgetLoader.php:146
     1759msgid "Your orders"
     1760msgstr ""
     1761
     1762#. translators: %s: order number.
     1763#: src/PHP/Widget/WidgetLoader.php:148
     1764#, php-format
     1765msgid "Order #%s"
     1766msgstr ""
     1767
     1768#. translators: %d: number of other items in the order.
     1769#: src/PHP/Widget/WidgetLoader.php:150
     1770#, php-format
     1771msgid "and %d more"
     1772msgstr ""
     1773
     1774#: src/PHP/Widget/WidgetLoader.php:151
     1775msgid "Tracking number"
     1776msgstr ""
     1777
     1778#: src/PHP/Widget/WidgetLoader.php:152
     1779msgid "Track"
     1780msgstr ""
     1781
     1782#: src/PHP/Widget/WidgetLoader.php:153
     1783msgid "View order"
     1784msgstr ""
     1785
     1786#: src/PHP/Widget/WidgetLoader.php:154
    11681787msgid "Did this answer your question?"
    11691788msgstr ""
    11701789
    1171 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:112
    1172 #: premium/CRM/CrmStore.php:611
    1173 #: src/PHP/Widget/WidgetLoader.php:112
    1174 msgid "Yes"
    1175 msgstr ""
    1176 
    1177 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:113
    1178 #: src/PHP/Widget/WidgetLoader.php:113
     1790#: src/PHP/Widget/WidgetLoader.php:156
    11791791msgid "Not yet"
    11801792msgstr ""
    11811793
    1182 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:114
    1183 #: src/PHP/Widget/WidgetLoader.php:114
     1794#: src/PHP/Widget/WidgetLoader.php:157
    11841795msgid "Skip"
    11851796msgstr ""
    11861797
    1187 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:115
    1188 #: src/PHP/Widget/WidgetLoader.php:115
     1798#: src/PHP/Widget/WidgetLoader.php:158
    11891799msgid "Thanks for letting us know."
    11901800msgstr ""
    11911801
    1192 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:116
    1193 #: src/PHP/Widget/WidgetLoader.php:116
     1802#: src/PHP/Widget/WidgetLoader.php:159
    11941803msgid "Other ways to reach us"
    11951804msgstr ""
    11961805
    1197 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:118
    1198 #: src/PHP/Widget/WidgetLoader.php:118
     1806#: src/PHP/Widget/WidgetLoader.php:161
    11991807msgid "Speak your message"
    12001808msgstr ""
    12011809
    1202 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:119
    1203 #: src/PHP/Widget/WidgetLoader.php:119
     1810#: src/PHP/Widget/WidgetLoader.php:162
    12041811msgid "Stop listening"
    12051812msgstr ""
    12061813
    1207 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:120
    1208 #: src/PHP/Widget/WidgetLoader.php:120
     1814#: src/PHP/Widget/WidgetLoader.php:163
    12091815msgid "Listening…"
    12101816msgstr ""
    12111817
    1212 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:121
    1213 #: src/PHP/Widget/WidgetLoader.php:121
     1818#: src/PHP/Widget/WidgetLoader.php:164
    12141819msgid "Microphone access is blocked. Allow it in your browser settings, or type your message."
    12151820msgstr ""
    12161821
    1217 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:122
    1218 #: src/PHP/Widget/WidgetLoader.php:122
     1822#: src/PHP/Widget/WidgetLoader.php:165
    12191823msgid "No speech was heard. Please try again."
    12201824msgstr ""
    12211825
    1222 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:123
    1223 #: src/PHP/Widget/WidgetLoader.php:123
     1826#: src/PHP/Widget/WidgetLoader.php:166
    12241827msgid "No microphone was found. Please type your message."
    12251828msgstr ""
    12261829
    1227 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:124
    1228 #: src/PHP/Widget/WidgetLoader.php:124
     1830#: src/PHP/Widget/WidgetLoader.php:167
    12291831msgid "Voice input needs an internet connection. Please type your message."
    12301832msgstr ""
    12311833
    1232 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:125
    1233 #: src/PHP/Widget/WidgetLoader.php:125
     1834#: src/PHP/Widget/WidgetLoader.php:168
    12341835msgid "Your browser cannot recognise this language. Please type your message."
    12351836msgstr ""
    12361837
    1237 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:126
    1238 #: src/PHP/Widget/WidgetLoader.php:126
     1838#: src/PHP/Widget/WidgetLoader.php:169
    12391839msgid "Voice input is not available right now. Please type your message."
    12401840msgstr ""
    12411841
    1242 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:164
    1243 #: src/PHP/Widget/WidgetLoader.php:164
     1842#: src/PHP/Widget/WidgetLoader.php:215
    12441843msgid "You are chatting with an AI assistant. A team member can join when needed."
    12451844msgstr ""
    1246 
    1247 #: premium/CRM/CrmStore.php:609
    1248 msgid "Customer ID"
    1249 msgstr ""
    1250 
    1251 #: premium/CRM/CrmStore.php:611
    1252 msgid "Verified"
    1253 msgstr ""
    1254 
    1255 #: premium/CRM/CrmStore.php:611
    1256 msgid "No"
    1257 msgstr ""
    1258 
    1259 #. translators: %s: contact type, for example "email" or "phone".
    1260 #: premium/CRM/CrmStore.php:616
    1261 #, php-format
    1262 msgid "Contact (%s)"
    1263 msgstr ""
    1264 
    1265 #: premium/CRM/CrmStore.php:620
    1266 msgid "Ninhle AI Sales Assistant customer profile"
    1267 msgstr ""
    1268 
    1269 #: premium/CRM/CrmStore.php:623
    1270 msgid "Ninhle AI Sales Assistant contact permissions"
    1271 msgstr ""
    1272 
    1273 #: premium/CRM/CrmStore.php:624
    1274 msgid "Purpose"
    1275 msgstr ""
    1276 
    1277 #: premium/CRM/CrmStore.php:627
    1278 msgid "Source"
    1279 msgstr ""
    1280 
    1281 #: premium/CRM/CrmStore.php:628
    1282 msgid "Recorded"
    1283 msgstr ""
    1284 
    1285 #: premium/CRM/CrmStore.php:634
    1286 msgid "Ninhle AI Sales Assistant purchase opportunities"
    1287 msgstr ""
    1288 
    1289 #: premium/CRM/CrmStore.php:635
    1290 msgid "Need"
    1291 msgstr ""
    1292 
    1293 #: premium/CRM/CrmStore.php:636
    1294 msgid "Stage"
    1295 msgstr ""
    1296 
    1297 #: premium/CRM/CrmStore.php:637
    1298 msgid "Payment"
    1299 msgstr ""
    1300 
    1301 #: premium/CRM/CrmStore.php:638
    1302 msgid "Products"
    1303 msgstr ""
    1304 
    1305 #: premium/CRM/CrmStore.php:664
    1306 msgid "Ninhle AI Sales Assistant customer history"
    1307 msgstr ""
    1308 
    1309 #: premium/CRM/CrmStore.php:665
    1310 #: premium/CRM/CrmStore.php:675
    1311 msgid "Type"
    1312 msgstr ""
    1313 
    1314 #: premium/CRM/CrmStore.php:667
    1315 msgid "Content"
    1316 msgstr ""
    1317 
    1318 #: premium/CRM/CrmStore.php:674
    1319 msgid "Ninhle AI Sales Assistant support and follow-up requests"
    1320 msgstr ""
    1321 
    1322 #: premium/CRM/CrmStore.php:677
    1323 msgid "Resolution"
    1324 msgstr ""
    1325 
    1326 #: premium/Integrations/TelegramService.php:77
    1327 msgid "None"
    1328 msgstr ""
    1329 
    1330 #. translators: %s: chat widget brand name.
    1331 #: premium/Integrations/TelegramService.php:79
    1332 #, php-format
    1333 msgid "New lead from %s"
    1334 msgstr ""
    1335 
    1336 #: premium/Integrations/TelegramService.php:80
    1337 msgid "Name:"
    1338 msgstr ""
    1339 
    1340 #: premium/Integrations/TelegramService.php:81
    1341 msgid "Email:"
    1342 msgstr ""
    1343 
    1344 #: premium/Integrations/TelegramService.php:82
    1345 msgid "Phone:"
    1346 msgstr ""
    1347 
    1348 #: premium/Webhooks/WebhookDispatcher.php:113
    1349 msgid "Outgoing webhooks are available in the Pro plan."
    1350 msgstr ""
  • ninhle-ai-sales-assistant/tags/2.6.0/ninhle-ai-sales-assistant.php

    r3761175 r3765552  
    11<?php
    22/**
    3  * Plugin Name: Ninhle AI Sales Assistant for WooCommerce
    4  * Description: AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff.
    5  * Version:     2.5.0
     3 * Plugin Name: Ninhle AI Sales Assistant – AI Chatbot for WooCommerce
     4 * Description: AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in the chat, tracks orders and shows why shoppers don't buy.
     5 * Version:     2.6.0
    66 * Author:      Ninhle
    77 * Author URI:  https://ninhle.com
    … …  
    3737
    3838// Define Plugin Constants
    39 define('SMBCOMMERCE_VERSION', '2.5.0');
     39define('SMBCOMMERCE_VERSION', '2.6.0');
    4040define('SMBCOMMERCE_FILE', __FILE__);
    4141define('SMBCOMMERCE_PATH', plugin_dir_path(__FILE__));
  • ninhle-ai-sales-assistant/tags/2.6.0/package-lock.json

    r3761175 r3765552  
    11{
    22  "name": "ninhle-ai-sales-assistant-frontend",
    3   "version": "2.5.0",
     3  "version": "2.6.0",
    44  "lockfileVersion": 3,
    55  "requires": true,
    … …  
    77    "": {
    88      "name": "ninhle-ai-sales-assistant-frontend",
    9       "version": "2.5.0",
     9      "version": "2.6.0",
    1010      "dependencies": {
    1111        "preact": "^10.23.1",
  • ninhle-ai-sales-assistant/tags/2.6.0/package.json

    r3761175 r3765552  
    22  "name": "ninhle-ai-sales-assistant-frontend",
    33  "private": true,
    4   "version": "2.5.0",
     4  "version": "2.6.0",
    55  "type": "module",
    66  "scripts": {
  • ninhle-ai-sales-assistant/tags/2.6.0/readme.txt

    r3762743 r3765552  
    55Tested up to: 7.1
    66Requires PHP: 8.1
    7 Stable tag: 2.5.0
     7Stable tag: 2.6.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 AI chatbot and shopping assistant for WooCommerce: answers from your real catalog, tracks orders, hands off to your team, shows AI-assisted orders.
     11AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in chat, tracks orders and shows why shoppers don't buy.
    1212
    1313== Description ==
    1414
    15 **Turn shopper questions into orders.** Ninhle AI Sales Assistant is an AI chatbot for WooCommerce: a shopping assistant for your customers and a sales assistant for you. It answers in seconds from the prices, stock and product details already in your store, looks up orders, brings in your team when needed, and shows you which orders followed a chat.
     15**Turn shopper questions into orders.** Ninhle AI Sales Assistant is an AI chatbot and shopping assistant for WooCommerce. It answers from your real catalog, adds products to the cart right in the chat, tracks orders, and shows you why shoppers ask but don't buy.
    1616
    1717Shoppers ask the same things all day: *Is it in stock? Does it come in M? How long is shipping? Where is my order?* When nobody answers, many of them leave. Unlike a generic ecommerce chatbot that only knows what it was trained on, this WooCommerce chatbot answers from live lookups against your store, and says so when it does not know.
    … …  
    1919= Get started in three steps =
    2020
    21 1. **Connect an AI provider.** Paste your own API key from OpenAI, Anthropic (Claude) or Google (Gemini). You pay the provider directly. Or start in local-only mode with no key at all.
     211. **Connect an AI provider.** Paste your own API key from OpenAI, Anthropic (Claude) or Google (Gemini), or use the AI connection built into WordPress 7.0 and later. You pay the provider directly. Or start in local-only mode with no key at all.
    22222. **Sync your catalog.** One click, or `wp ninhle-ai-sales-assistant sync-products`, and the assistant knows your products, prices and stock.
    23233. **Go live.** The chat appears on your store with your name, logo, colour and suggested questions.
    2424
    25 = An AI chatbot that answers from your real catalog =
    26 
    27 Product facts come from your WooCommerce store: prices, stock, product details, store policies and your own pages. If the catalog has not been synced, the assistant says so and offers a person instead of guessing. It never invents prices or stock.
    28 
    29 = A shopping assistant that sells, not just chats =
    30 
    31 An editable sales playbook sets the tone, the qualifying questions, how to handle objections and how to close. The assistant recommends products from your catalog and links shoppers straight to them, so a question turns into a product page, a cart and a checkout.
     25= Add to cart in the chat =
     26
     27Product answers come with product cards: photo, current price, stock and rating, read from WooCommerce when the reply is shown. Shoppers tap **Add to cart** without leaving the chat. WooCommerce updates the cart in the shopper's own browser, so your prices, stock rules and discounts apply as usual, and a cart view in the chat leads to Checkout.
     28
     29In the free version, products with options link to the product page. The Starter plan lets shoppers pick options, change quantities and apply the discount codes you allow, right on the card.
     30
     31= Answers from your real catalog =
     32
     33Product facts come from your WooCommerce store: prices, stock, product details, store policies and your own pages. When a search finds nothing, the assistant suggests the nearest categories and asks one question instead of guessing. It never invents prices or stock, and products you hide from the catalog are never shown.
    3234
    3335= Order tracking in the chat =
    3436
    35 Customers ask "Where is my order?" and get the status right in the chat once they give the order number with the email or phone number used for it. Nobody on your team has to look it up.
     37"Where is my order?" gets an order card in the chat: status, items, total and tracking number with a Track link (from WooCommerce Shipment Tracking, Advanced Shipment Tracking or an order field you choose). Logged-in customers see their recent orders straight away; guests give the order number with the email or phone number used for it. Without that proof, nothing is revealed.
     38
     39= Replies in the shopper's language =
     40
     41The assistant detects the language of each message (15 languages, without an AI call) and answers in it, keeping product names, prices and order numbers exactly as in your store. You can keep every reply in your store's language instead.
     42
     43= Sales Insights: why shoppers don't buy =
     44
     45The Sales Insights tab lists the questions the assistant could not answer, grouped and ranked; type your answer there and the assistant uses it from the next reply. It also shows what shoppers searched for but could not find, and which products they wanted while out of stock. Contact details are hidden.
     46
     47In the Starter plan, an AI labels each night why finished chats did not lead to an order (price, shipping, size, stock, missing information and more), with quotes, and a weekly summary can be emailed to you.
    3648
    3749= A person is one tap away =
    3850
    39 "Talk to a person" is always visible, and the assistant hands off complaints, explicit requests and answers it is not sure about. Your team replies from the Inbox in WP Admin, and a webhook you choose can be notified.
     51"Talk to a person" is always visible, and the assistant hands off complaints, explicit requests and questions your store data cannot answer. Your team replies from the Inbox in WP Admin, and a badge on the minimized chat button shows new replies. In the Starter plan, the Inbox shows a customer card for logged-in customers and those who proved an order: orders, total spent, last order and recent products.
    4052
    4153= See the orders the chatbot helped close =
    … …  
    4355The AI Value & Performance tab shows what the AI cost, how many orders followed a chat and how conversations ended, each with its sample size, from the day you install. Leads left in the chat stay in your own database and export to CSV.
    4456
     57= Ready for AI agents (MCP) =
     58
     59On WordPress 6.9 and later, turn on "AI agents" in Settings to register read-only abilities with the WordPress Abilities API: search products, product details and store policies. The MCP Adapter plugin can offer them to MCP clients. Only published, visible products and your store policies are shared, never orders, carts or customer data. Off by default, limited to 60 requests per hour per IP address.
     60
    4561= Your key, your data, no per-conversation fee =
    4662
    47 Use your own OpenAI, Anthropic Claude or Google Gemini API key and pay the provider directly. Requests go from your server to that provider; there is no middleman server and no per-chat fee from us. Simple greetings and store policy, cart and order-status questions are answered from store data without an AI call whenever the store has the answer, which keeps the cost down.
     63Use your own OpenAI, Anthropic Claude or Google Gemini API key and pay the provider directly, or use the AI provider connected in WordPress 7.0+ (Settings > Connectors). There is no middleman server and no per-chat fee from us. Greetings, store policies, cart questions and order status are answered from store data without an AI call when the store has the answer, and AI replies run in the background so a slow provider does not hold up your pages.
    4864
    4965= Everything in the free WooCommerce chatbot =
    5066
    51 * **Bring your own key:** Anthropic Claude, OpenAI or Google Gemini. You control the account and the spend.
    52 * **Store tools:** product search and details, order status (only after the customer proves they own the order), digital downloads, store policies and site content.
    53 * **Sales playbook and rules:** persona, qualifying questions, objection handling and closing, plus keyword rules for instant answers.
    54 * **Lead capture:** a contact form in the chat, with leads stored in your database and exportable to CSV.
    55 * **Human handoff:** visitors always see whether they are talking to the AI assistant, waiting for your team or talking to a team member. A webhook URL you choose can be notified.
    56 * **AI Value & Performance:** AI cost recorded per request from your provider's usage data and a dated price table; orders that followed a chat; conversations resolved (confirmed by the customer), handed off or unresolved.
    57 * **Conversation inbox:** read every conversation, take over from the bot and hand the chat back when you are done.
    58 * **Formatted answers:** bold text, lists, tables, links and product images display cleanly in the chat.
    59 * **Voice input (optional, off by default):** visitors can speak their message; the browser turns it into text they check before sending.
    60 * **Your brand:** widget name, logo, colour, font and suggested question chips, with a live preview.
    61 * **Local-only mode:** answer product, cart and order questions from store data without calling any AI provider.
    62 * **Light on your pages:** the chat window loads only when a visitor opens it.
    63 * **Privacy built in:** WordPress personal-data export and erasure, attachment retention, rate limiting.
    64 * **Translation-ready:** every string can be translated on translate.wordpress.org.
    65 * **WooCommerce compatible:** supports High-Performance Order Storage (HPOS) and the Cart and Checkout blocks.
     67* **Bring your own key:** Anthropic Claude, OpenAI or Google Gemini, or the AI connection in WordPress 7.0+.
     68* **Product cards and add to cart in the chat,** with a cart view.
     69* **Order tracking:** order cards with tracking numbers; "my orders" for logged-in customers.
     70* **Replies in the shopper's language** or always in your store's language.
     71* **Sales Insights:** unanswered questions, searches with no result and out-of-stock demand.
     72* **Store tools:** product search and details, order status for the order's owner, digital downloads, store policies and site content.
     73* **Sales playbook and rules:** persona, qualifying questions, objection handling, closing and keyword rules.
     74* **Lead capture** in the chat, stored in your database and exportable to CSV.
     75* **Human handoff and Inbox:** a clear status for visitors, a new-replies badge, take over and hand back, an optional webhook.
     76* **AI Value & Performance:** AI cost per request, AI-assisted orders and conversation outcomes.
     77* **Formatted answers:** lists, tables, links and product images.
     78* **Voice input** (optional, off by default).
     79* **Your brand:** name, logo, colour, font and suggested questions, with a live preview.
     80* **Local-only mode:** answers from store data without any AI provider.
     81* **Ready for AI agents** (WordPress 6.9+, off by default).
     82* **Light, private and translation-ready:** the chat loads only when needed; personal-data export and erasure; HPOS and Cart and Checkout blocks supported.
    6683
    6784= Optional paid plans =
    … …  
    7087plugin; nothing in the free version is locked or time-limited.
    7188
    72 **Starter** — product variations with per-variation add-to-cart links, Telegram
    73 notifications for new leads and handoffs, a sales dashboard, cached answers
    74 for repeated questions, and the revenue of orders that followed a chat, per currency,
    75 on the AI Value & Performance tab.
     89**Starter** — options, quantities and allowed discount codes on product cards in the chat;
     90nightly AI labels of why shoppers did not buy, with a weekly email summary; a customer card in
     91the Inbox; product variations with per-variation add-to-cart links; Telegram notifications for
     92new leads and handoffs; a sales dashboard; cached answers for repeated questions; and the
     93revenue of orders that followed a chat, per currency, on the AI Value & Performance tab.
    7694
    7795**Pro** — everything in Starter, plus coupon validation, automatic failover to a
    … …  
    91109conversation history, your sales playbook and business information, and product data from
    92110your own store, each time a visitor sends a message. Nothing is sent in local-only mode.
     111If you choose "WordPress AI connection" (WordPress 7.0+), the same data goes through WordPress
     112to the provider you connected in Settings > Connectors, under that provider's terms.
    93113
    94114* Anthropic (Claude) — https://www.anthropic.com/legal/privacy — https://www.anthropic.com/legal/consumer-terms
    95115* OpenAI — https://openai.com/policies/privacy-policy — https://openai.com/policies/terms-of-use
    96116* Google (Gemini) — https://policies.google.com/privacy — https://ai.google.dev/gemini-api/terms
     117
     118**Sales Insights AI labeling (Starter plan only, optional, off by default)** — the free
     119version contains no code for this. If you turn it on, once a night the text of finished chats
     120that did not lead to an order is sent to your AI provider, with email addresses and phone
     121numbers replaced by placeholders, so the AI can label why the shopper did not buy. A monthly
     122spending limit you set stops the job.
    97123
    98124**Link reading (optional, off by default)** — when you enable "Let the bot read links
    … …  
    154180permission_callback, and the endpoints the AI uses as tools are internal-only -
    155181they cannot be reached from outside the site. Visitor sessions use hashed tokens
    156 and chat messages are rate limited per session.
     182and chat messages are rate limited per session. When a background AI reply is started
     183through a loopback request, that request is signed and expires after 60 seconds.
     184
     185The plugin server never changes a shopper's cart: "Add to cart" in the chat calls
     186the WooCommerce Store API from the shopper's own browser. The AI can only propose
     187products that come from your catalog, and the card always reads the current price
     188and stock from WooCommerce.
    157189
    158190Chat messages are rendered as DOM elements rather than injected HTML, and only
    … …  
    178210paragraph about this to Settings > Privacy.
    179211
     212Sales Insights stores the questions the assistant could not answer, with email addresses
     213and phone numbers hidden, and in the Starter plan the reason a chat did not lead to an
     214order with a short quote. When a logged-in customer chats, or a customer proves they own
     215an order, the conversation records that account or order number, so your team can see
     216the customer card (Starter plan). The language of each conversation is stored to reply in
     217it. All of this is included in the personal-data export and removed by the eraser.
     218
     219In the visitor's browser, the chat keeps its session token and, for the new-replies badge,
     220the conversation number, the last message seen and when it was seen (localStorage). No
     221message text is stored there.
     222
    180223== Installation ==
    181224
    … …  
    192235AI provider for your own API key, or use local-only mode with no key at all.
    193236
     237= Can this WooCommerce chatbot add products to the cart? =
     238
     239Yes. Product answers show cards with an Add to cart button, and the cart is updated by WooCommerce
     240in the shopper's browser, so your prices, stock and cart rules apply. In the free version, products
     241with options link to the product page; the Starter plan lets shoppers choose options and quantities
     242on the card. Nothing is added until the shopper taps the button.
     243
    194244= Can customers track their orders in the chat? =
    195245
    196 Yes. When a customer gives the order number together with the email address or phone number used
    197 for the order, the assistant shows the order status in the chat. Without that proof it reveals
    198 nothing about the order.
     246Yes. The assistant shows an order card with the status, items and tracking number. Logged-in
     247customers can ask for their recent orders. Guests give the order number together with the email
     248address or phone number used for the order; without that proof the assistant reveals nothing.
     249
     250= Does it reply in the customer's language? =
     251
     252Yes, by default on new installs. It detects the language of each message and answers in it. Sites
     253upgraded from an earlier version keep replying in the store's language until you change "Reply
     254language" in Settings.
    199255
    200256= Which AI models does the chatbot work with? =
    201257
    202 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. You choose the
     258Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. On WordPress 7.0 and
     259later you can also use the AI provider connected in Settings > Connectors. You choose the
    203260provider and can switch at any time.
    204261
     262= What does Sales Insights show? =
     263
     264The questions shoppers asked that the assistant could not answer, grouped and ranked, what they
     265searched for but could not find, and which products they wanted while out of stock. You can add
     266an answer from the same screen. The Starter plan adds the reasons chats did not lead to an order and a
     267weekly email summary.
     268
     269= Can AI agents read my product catalog (MCP)? =
     270
     271Yes, if you allow it. On WordPress 6.9 and later, turn on "AI agents" in Settings: the plugin
     272registers read-only abilities for product search, product details and store policies. With the
     273MCP Adapter plugin, MCP clients can use them. Orders, carts and customer data are never shared.
     274
    205275= Does the shopping assistant recommend products? =
    206276
    207277Yes. It searches your synced WooCommerce catalog, recommends products that match what the shopper
    208 asked for and links to them. It only recommends products that exist in your store.
     278asked for and shows them as cards. It only recommends products that exist in your store.
    209279
    210280= Do I need a paid AI subscription? =
    … …  
    213283and are billed directly by that provider. Local-only mode needs no key.
    214284
    215 = How much does the AI cost to run? =
    216 
    217 It depends on the provider and model you choose, and you pay the provider directly. In our
    218 internal test of 60 conversations with GPT-4o mini, the AI cost about US$0.0006 per conversation;
    219 larger models cost more. The AI Value & Performance tab shows what your own store actually spent.
     285= How much does an AI chatbot for WooCommerce cost to run? =
     286
     287It depends on the provider and model you choose, and you pay the provider directly. In our test
     288of 60 conversations on a real store with version 2.6.0, GPT-4o mini cost about US$0.0009 per
     289conversation and Gemini 3.8 Flash about US$0.015. The AI Value & Performance tab shows what your
     290own store actually spent.
    220291
    221292= Can I try it without an API key? =
    222293
    223 Yes. Local-only mode answers product, cart and order questions from your store data without
    224 calling any AI provider. Add a key later when you want full conversations.
     294Yes. Local-only mode answers greetings, store policies, cart and order questions from your store
     295data without calling any AI provider. Add a key later when you want full conversations.
    225296
    226297= Will it slow down my store? =
    227298
    228 The chat button is small, and the chat window and voice input load only when a visitor opens
    229 the chat. Nothing is loaded from third-party servers unless you pick a Google font.
     299The chat button is small. The chat window, product cards, cart view and voice input load only
     300when they are needed, and AI replies run in the background after a message is sent. While the
     301chat is minimized after a recent conversation, it checks for new replies every 25 seconds, only
     302while the browser tab is visible. Nothing is loaded from third-party servers unless you pick a
     303Google font.
    230304
    231305= Is my store data sent to you? =
    … …  
    237311= Does it work without WooCommerce? =
    238312
    239 The chat and lead capture work on any WordPress site. Product search, catalog grounding and
    240 order lookup require WooCommerce.
     313The chat and lead capture work on any WordPress site. Product search, product cards, catalog
     314grounding and order lookup require WooCommerce.
    241315
    242316= Will it invent prices or stock? =
    243317
    244 No. The playbook forbids it and product facts come from lookups against your store. If the
    245 catalog has not been synced, the bot is told to say so and offer a handoff instead of guessing.
     318No. Product facts come from lookups against your store, and product cards read the current price
     319and stock from WooCommerce when the reply is shown. If the catalog has not been synced, the bot
     320is told to say so and offer a handoff instead of guessing.
    246321
    247322= What counts as an "AI-assisted order"? =
    … …  
    249324An order placed within your attribution window (24 hours, 72 hours or 7 days) after the
    250325customer received an answer with products from your store, or clicked a product or cart link
    251 in the chat. Each order is counted once. Cancelled, failed and fully refunded orders are left
    252 out, and partial refunds are deducted. AI-assisted does not mean the AI was the only reason
    253 for the order.
     326in the chat, or added a product to the cart from the chat. Each order is counted once.
     327Cancelled, failed and fully refunded orders are left out, and partial refunds are deducted.
     328AI-assisted does not mean the AI was the only reason for the order.
    254329
    255330= Is anything in the free version locked? =
    … …  
    274349
    275350== Changelog ==
     351
     352= 2.6.0 =
     353* Added: add to cart in the chat. Product answers show cards with photo, current price, stock and rating, read from WooCommerce when the reply is shown. Simple products have an Add to cart button that uses the WooCommerce Store API in the shopper's browser; the plugin server never changes the cart. A cart view in the chat shows the items, subtotal and Checkout.
     354* Added: order tracking cards in the chat, with status, date, items, total and tracking number with a Track link (WooCommerce Shipment Tracking, Advanced Shipment Tracking or an order field you choose in Settings). Logged-in customers can ask for their recent orders without giving an email address.
     355* Added: Sales Insights tab: questions the assistant could not answer, grouped and ranked, with "Add answer" to teach the assistant; searches with no matching product; products wanted while out of stock. Email addresses and phone numbers are hidden.
     356* Added: replies in the shopper's language (15 languages detected without an AI call). New installs reply in the customer's language; upgraded sites keep the store's language until you change "Reply language" in Settings.
     357* Added (Starter): choose options and quantities on product cards, remove items and apply discount codes you allow; the assistant can prepare the card with the shopper's choice.
     358* Added (Starter): nightly AI labels of why chats did not lead to an order, with quotes, a monthly spending limit and an optional weekly email summary.
     359* Added (Starter): a customer card in the Inbox for logged-in customers and customers who proved an order: order count, total spent, last order and recent products. Shown only to users who can view WooCommerce orders.
     360* Added: a badge on the chat button when a reply arrives while the chat is minimized.
     361* Added: "WordPress AI connection" as an AI provider on WordPress 7.0 and later (Settings > Connectors).
     362* Added: optional read-only abilities for AI agents (WordPress Abilities API, WordPress 6.9+): product search, product details and store policies. Off by default.
     363* Added: after a week of use, once the assistant has answered at least 10 conversations or helped with an order, administrators are asked on the plugin screens whether they would leave a review on WordPress.org, with "Maybe later" and "Don't ask again". At most three times, never within a day of an AI error, and it can be turned off with a filter.
     364* Changed: AI replies now run in the background after a message is sent, instead of inside the visitor's next request. At most 2 replies run at the same time by default. The chat checks for replies often right after a message and less often while idle.
     365* Changed: product search also finds products when the shopper's words are in a different order or include extra words.
     366* Changed: the plugin is now named "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce".
     367* Fixed: products hidden from the catalog could still be recommended.
     368* Fixed: a product on sale for 0 was quoted at its regular price, and a sale scheduled for later was quoted before it started.
     369* Fixed: questions that mention payment (for example "Do you have a plugin for QR payments?") were answered with the checkout link.
     370* Fixed: logged-in customers were treated as guests in the chat, so their account could not be used to look up their orders.
     371* Fixed: with Google Gemini, a saved model that Google no longer offers to new API keys made every reply fail. The plugin now falls back to the current Gemini Flash model and logs a reminder to update the setting.
     372* Fixed: Gemini could return an empty reply when its thinking used up the answer length.
     373* Fixed: with LiteSpeed Cache, the cart shown in the chat could be served from the page cache and look empty or out of date.
     374* Fixed: when the AI provider was slow, the visitor could be told the assistant was still looking and then get no answer.
    276375
    277376= 2.5.0 =
    … …  
    370469== Upgrade Notice ==
    371470
     471= 2.6.0 =
     472Add to cart in the chat, order tracking with tracking numbers, replies in the shopper's language and Sales Insights. Data and settings are kept; replies stay in your store's language until you change "Reply language". The catalog refreshes within the hour.
     473
    372474= 2.5.0 =
    373475Replies display with proper formatting and product images no longer break. Adds optional voice input (off by default). Your data and settings are kept. To refresh product images right away, click "Sync products now" in Settings; otherwise they refresh within the hour.
    … …  
    378480= 2.4.0 =
    379481New AI Value & Performance tab and a redesigned chat widget with a "Talk to a person" button. Statistics start on the day you upgrade. Check Settings > Business information: sample text from earlier versions is no longer used, so enter your real details.
    380 
    381 = 2.3.0 =
    382 Telegram notifications and other paid features now ship only in the Starter and Pro plans. Adds personal-data export/erasure and WooCommerce HPOS compatibility.
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/components/Inbox.tsx

    r3761175 r3765552  
    22import { api } from '../api';
    33import { ConfirmBox, NoticeList, useConfirm, useNotices } from './Feedback';
     4import { CustomerCard, CustomerCardData } from './CustomerCard';
    45
    56// Nhãn hiển thị (2.5.0): chủ shop đọc "Waiting for your team" thay vì mã human_active.
    … …  
    2425};
    2526
    26 export function Inbox() {
     27/** openId: mở thẳng hội thoại này (link "Open chat" trong Sales Insights). */
     28export function Inbox({ openId = null }: { openId?: number | null } = {}) {
    2729    const { notices, push, dismiss } = useNotices();
    2830    const confirmation = useConfirm();
    … …  
    3739    const timeoutRef = useRef<number | null>(null);
    3840    const lastMessageIdRef = useRef<number | null>(null);
     41    // Thẻ khách (S12, Starter): chỉ có khi khách đã biết chắc; tải lại khi có tin mới (khách vừa chứng minh đơn).
     42    const [customer, setCustomer] = useState<CustomerCardData | null>(null);
     43    const shownIdRef = useRef<number | null>(null);
     44    const loadCustomer = (id: number) => {
     45        // Trả lời muộn của hội thoại trước không được hiện trên hội thoại đang mở.
     46        api.get(`/admin/conversations/${id}/customer`)
     47            .then(data => { if (shownIdRef.current === id) setCustomer(data?.card || null); })
     48            .catch(() => { if (shownIdRef.current === id) setCustomer(null); });
     49    };
    3950
    4051    useEffect(() => { loadConversations(); }, []);
     52
     53    useEffect(() => {
     54        if (!openId) return;
     55        api.get(`/admin/conversations/${openId}`)
     56            .then(conv => conv && selectConversation(conv))
     57            .catch(() => push('error', 'This conversation no longer exists.'));
     58    }, [openId]);
    4159
    4260    const loadConversations = async () => {
    … …  
    4967    const selectConversation = async (conv: any) => {
    5068        setActiveConv(conv);
     69        shownIdRef.current = conv.id;
     70        setCustomer(null);
     71        loadCustomer(conv.id);
    5172        try {
    5273            const data = await api.getMessages(conv.id);
    … …  
    6889                scrollToBottom();
    6990                pollIntervalRef.current = 5000;
     91                loadCustomer(activeConv.id);
    7092            }
    7193        } catch { pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000); }
    … …  
    86108            try {
    87109                await api.deleteConversation(id);
    88                 if (activeConv?.id === id) { setActiveConv(null); setMessages([]); }
     110                if (activeConv?.id === id) { setActiveConv(null); setMessages([]); setCustomer(null); shownIdRef.current = null; }
    89111                setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; });
    90112                loadConversations();
    … …  
    111133            try {
    112134                const result = await api.bulkDeleteConversations(Array.from(selectedIds));
    113                 if (activeConv && selectedIds.has(activeConv.id)) { setActiveConv(null); setMessages([]); }
     135                if (activeConv && selectedIds.has(activeConv.id)) { setActiveConv(null); setMessages([]); setCustomer(null); shownIdRef.current = null; }
    114136                setSelectedIds(new Set());
    115137                loadConversations();
    … …  
    197219                        </div>
    198220                    </div>
     221                    {customer && (
     222                        <details className="smb-customer-inline">
     223                            <summary>Customer: {customer.name || 'Guest customer'}</summary>
     224                            <CustomerCard card={customer} />
     225                        </details>
     226                    )}
    199227                    <div className="smb-commerce-chat-history">
    200228                        {messages.map(m => (
    … …  
    221249                )}
    222250            </div>
     251            {activeConv && customer && (
     252                <aside className="smb-customer-aside" aria-label="Customer">
     253                    <CustomerCard card={customer} />
     254                </aside>
     255            )}
    223256        </div>
    224257        </>
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/components/Settings.tsx

    r3761175 r3765552  
    3838        smbcommerce_hide_on_checkout: '0',
    3939        smbcommerce_voice_input_enabled: '0',
     40        smbcommerce_reply_language: 'customer',
     41        smbcommerce_coupon_allowlist: '',
     42        smbcommerce_tracking_code_meta: '',
     43        smbcommerce_tracking_carrier_meta: '',
     44        smbcommerce_tracking_carrier: '',
     45        smbcommerce_tracking_url_template: '',
     46        smbcommerce_abilities_enabled: '0',
    4047        smbcommerce_gemini_model: '',
    4148        smbcommerce_anthropic_model: '',
    … …  
    7481    const [features, setFeatures] = useState<any>({});
    7582    const [premiumBuild, setPremiumBuild] = useState(true);
     83    // Kết nối AI của WordPress 7.0+ (2.6.0, S7): máy chủ chỉ báo có/không.
     84    const [wpAiAvailable, setWpAiAvailable] = useState(false);
     85    // Abilities API cho AI agent (2.6.0, S8): WordPress 6.9+.
     86    const [abilitiesSupported, setAbilitiesSupported] = useState(false);
    7687    const [modelReport, setModelReport] = useState<any>(null);
    7788    // Thông tin kinh doanh còn nguyên đoạn mẫu bản cũ tự nạp: bot không dùng cho tới khi chủ shop sửa.
    … …  
    179190                    smbcommerce_hide_on_checkout: data.settings.smbcommerce_hide_on_checkout ?? '0',
    180191                    smbcommerce_voice_input_enabled: data.settings.smbcommerce_voice_input_enabled ?? '0',
     192                    smbcommerce_reply_language: data.settings.smbcommerce_reply_language || 'customer',
     193                    smbcommerce_coupon_allowlist: data.settings.smbcommerce_coupon_allowlist || '',
     194                    smbcommerce_tracking_code_meta: data.settings.smbcommerce_tracking_code_meta || '',
     195                    smbcommerce_tracking_carrier_meta: data.settings.smbcommerce_tracking_carrier_meta || '',
     196                    smbcommerce_tracking_carrier: data.settings.smbcommerce_tracking_carrier || '',
     197                    smbcommerce_tracking_url_template: data.settings.smbcommerce_tracking_url_template || '',
     198                    smbcommerce_abilities_enabled: data.settings.smbcommerce_abilities_enabled ?? '0',
    181199                    smbcommerce_gemini_model: data.settings.smbcommerce_gemini_model || '',
    182200                    smbcommerce_anthropic_model: data.settings.smbcommerce_anthropic_model || '',
    … …  
    199217                setFeatures(data.settings.smbcommerce_features || {});
    200218                setPremiumBuild(data.settings.smbcommerce_premium_build !== false);
     219                setWpAiAvailable(data.settings.smbcommerce_wp_ai_available === true);
     220                setAbilitiesSupported(data.settings.smbcommerce_abilities_supported === true);
    201221                setModelReport(data.settings.smbcommerce_model_report || null);
    202222                setSampleBusinessInfo(data.settings.smbcommerce_business_info_is_sample ? data.settings.smbcommerce_business_info : null);
    … …  
    247267                smbcommerce_hide_on_checkout: settings.smbcommerce_hide_on_checkout,
    248268                smbcommerce_voice_input_enabled: settings.smbcommerce_voice_input_enabled,
     269                smbcommerce_reply_language: settings.smbcommerce_reply_language,
     270                smbcommerce_coupon_allowlist: settings.smbcommerce_coupon_allowlist,
     271                smbcommerce_tracking_code_meta: settings.smbcommerce_tracking_code_meta,
     272                smbcommerce_tracking_carrier_meta: settings.smbcommerce_tracking_carrier_meta,
     273                smbcommerce_tracking_carrier: settings.smbcommerce_tracking_carrier,
     274                smbcommerce_tracking_url_template: settings.smbcommerce_tracking_url_template,
     275                smbcommerce_abilities_enabled: settings.smbcommerce_abilities_enabled,
    249276                smbcommerce_gemini_model: settings.smbcommerce_gemini_model,
    250277                smbcommerce_anthropic_model: settings.smbcommerce_anthropic_model,
    … …  
    562589
    563590                            <div className="smb-commerce-form-group">
     591                                <label htmlFor="smb-reply-language">Reply language</label>
     592                                <select
     593                                    id="smb-reply-language"
     594                                    value={settings.smbcommerce_reply_language}
     595                                    onChange={e => setSettings({...settings, smbcommerce_reply_language: e.target.value})}
     596                                >
     597                                    <option value="customer">The customer's language (recommended)</option>
     598                                    <option value="store">As in earlier versions: follow the sales playbook and site language</option>
     599                                </select>
     600                                <p className="description">
     601                                    With the customer's language, the assistant replies in the language each
     602                                    customer writes in and keeps product names, prices and order numbers exactly
     603                                    as they are in your store. Fixed messages, such as the handoff confirmation,
     604                                    follow the customer's language when this site has a translation for it.
     605                                </p>
     606                            </div>
     607
     608                            <div className="smb-commerce-form-group">
    564609                                <label>AI provider</label>
    565610                                <select
    … …  
    570615                                    <option value="anthropic">Anthropic (Claude 3.5)</option>
    571616                                    <option value="gemini">Google Gemini</option>
     617                                    {(wpAiAvailable || settings.smbcommerce_ai_provider === 'wordpress') && (
     618                                        <option value="wordpress">WordPress AI connection (Settings → Connectors)</option>
     619                                    )}
    572620                                </select>
     621                                {settings.smbcommerce_ai_provider === 'wordpress' && (
     622                                    <p className="description">
     623                                        {wpAiAvailable
     624                                            ? 'Uses the AI provider you set up in Settings → Connectors. The key stays in WordPress; this plugin never reads it. The keys below are not needed for this option.'
     625                                            : 'No AI provider is set up in Settings → Connectors any more. Set one up there, or choose another provider here.'}
     626                                    </p>
     627                                )}
    573628                            </div>
    574629
    … …  
    10411096
    10421097                        <div className="smb-commerce-card">
     1098                            <h3><span className="dashicons dashicons-cart"></span> Cart in the chat</h3>
     1099                            <p className="description" style={{marginTop: 0}}>
     1100                                Product cards in the chat have an Add to cart button and the chat shows the customer's
     1101                                cart. Everything goes through your store's own cart, so prices, stock and coupon rules
     1102                                are checked by WooCommerce as usual. The assistant never changes a cart by itself.
     1103                            </p>
     1104                            {!allowed('cart_actions_advanced') ? (
     1105                                <p className="description" id="smb-commerce-cart-plan-note">
     1106                                    Choosing product options on the card, changing quantities, removing items and applying
     1107                                    discount codes in the chat are available in the Starter plan.
     1108                                </p>
     1109                            ) : (
     1110                                <div className="smb-commerce-form-group">
     1111                                    <label htmlFor="smb-coupon-allowlist">Discount codes the assistant may offer</label>
     1112                                    <textarea
     1113                                        id="smb-coupon-allowlist"
     1114                                        rows={4}
     1115                                        placeholder={'WELCOME10, 10\nFREESHIP'}
     1116                                        value={settings.smbcommerce_coupon_allowlist}
     1117                                        onChange={e => setSettings({...settings, smbcommerce_coupon_allowlist: e.target.value})}
     1118                                    />
     1119                                    <p className="description">
     1120                                        One coupon code per line, up to 20. Add a comma and a number to cap the discount the
     1121                                        assistant may offer: a percentage for percentage coupons, an amount for fixed
     1122                                        coupons. The assistant offers a code only when a customer asks about discounts or
     1123                                        hesitates over the price, and shows an Apply button. Codes that are not listed here
     1124                                        are never offered. Leave it empty to turn this off.
     1125                                    </p>
     1126                                </div>
     1127                            )}
     1128                        </div>
     1129
     1130                        {abilitiesSupported && (
     1131                            <div className="smb-commerce-card">
     1132                                <h3><span className="dashicons dashicons-rest-api"></span> AI agents (Abilities API / MCP)</h3>
     1133                                <div className="smb-commerce-form-group toggle-group">
     1134                                    <label className="toggle-switch">
     1135                                        <input type="checkbox" checked={settings.smbcommerce_abilities_enabled === '1'}
     1136                                            onChange={e => setSettings({...settings, smbcommerce_abilities_enabled: e.target.checked ? '1' : '0'})} />
     1137                                        <span className="toggle-slider"></span>
     1138                                    </label>
     1139                                    <div>
     1140                                        <strong>Let AI agents read your public catalog</strong>
     1141                                        <p className="description" style={{margin: 0}}>
     1142                                            Adds three read-only abilities to WordPress: search products, product details and
     1143                                            store policies. With the MCP Adapter plugin installed, AI assistants and agents can
     1144                                            use them as tools. Only published, visible products and your policy texts are
     1145                                            shared; never orders, carts or customer data. Limited to 60 requests per hour per
     1146                                            visitor. Off by default.
     1147                                        </p>
     1148                                    </div>
     1149                                </div>
     1150                            </div>
     1151                        )}
     1152
     1153                        <div className="smb-commerce-card">
     1154                            <h3><span className="dashicons dashicons-location"></span> Order tracking</h3>
     1155                            <p className="description" style={{marginTop: 0}}>
     1156                                When a customer asks about an order, the chat shows an order card with its status and
     1157                                tracking number. Tracking numbers saved by WooCommerce Shipment Tracking or Advanced
     1158                                Shipment Tracking appear automatically. If your shipping plugin saves the tracking number
     1159                                in its own order field, enter that field's meta key below. Logged-in customers can ask
     1160                                about their orders without an order number.
     1161                            </p>
     1162                            <div className="smb-commerce-form-group">
     1163                                <label htmlFor="smb-tracking-code-meta">Tracking number meta key</label>
     1164                                <input id="smb-tracking-code-meta" type="text" placeholder="e.g. _ghn_order_code"
     1165                                    value={settings.smbcommerce_tracking_code_meta}
     1166                                    onChange={e => setSettings({...settings, smbcommerce_tracking_code_meta: e.target.value})} />
     1167                            </div>
     1168                            <div className="smb-commerce-form-group">
     1169                                <label htmlFor="smb-tracking-carrier-meta">Carrier meta key (optional)</label>
     1170                                <input id="smb-tracking-carrier-meta" type="text"
     1171                                    value={settings.smbcommerce_tracking_carrier_meta}
     1172                                    onChange={e => setSettings({...settings, smbcommerce_tracking_carrier_meta: e.target.value})} />
     1173                            </div>
     1174                            <div className="smb-commerce-form-group">
     1175                                <label htmlFor="smb-tracking-carrier">Carrier name (optional)</label>
     1176                                <input id="smb-tracking-carrier" type="text" placeholder="e.g. GHN"
     1177                                    value={settings.smbcommerce_tracking_carrier}
     1178                                    onChange={e => setSettings({...settings, smbcommerce_tracking_carrier: e.target.value})} />
     1179                                <p className="description">Used when there is no carrier meta key, or it is empty on an order.</p>
     1180                            </div>
     1181                            <div className="smb-commerce-form-group">
     1182                                <label htmlFor="smb-tracking-url">Tracking link (optional)</label>
     1183                                <input id="smb-tracking-url" type="url" placeholder="https://carrier.example/track?code={code}"
     1184                                    value={settings.smbcommerce_tracking_url_template}
     1185                                    onChange={e => setSettings({...settings, smbcommerce_tracking_url_template: e.target.value})} />
     1186                                <p className="description">
     1187                                    A web address with {'{code}'} where the tracking number goes. The chat shows a Track
     1188                                    button only when this is set. Meta keys may use letters, numbers, _ and -.
     1189                                </p>
     1190                            </div>
     1191                        </div>
     1192
     1193                        <div className="smb-commerce-card">
    10431194                            <h3><span className="dashicons dashicons-admin-tools"></span> Advanced features</h3>
    10441195
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/index.tsx

    r3752682 r3765552  
    1 import React, { useState } from 'react';
     1import React, { useEffect, useState } from 'react';
    22import { createRoot } from 'react-dom/client';
    33import { Inbox } from './components/Inbox';
    … …  
    66import { Settings } from './components/Settings';
    77import { ValueDashboard } from './components/ValueDashboard';
     8import { loadInsights } from './insightsLoader';
     9import { t } from './i18n';
    810import './style.css';
    911
    10 const TABS = ['value', 'inbox', 'rules', 'leads'];
     12const TABS = ['value', 'insights', 'inbox', 'rules', 'leads'];
    1113const TAB_KEY = 'smbcommerce_admin_tab';
    1214
    13 /** Tab đã mở lần trước (chỉ là tiện ích trên trình duyệt này; lỗi bộ nhớ thì về tab đầu). */
     15/** Tab mở đầu: ?smb_tab= (link trong bản tin Sales Insights), rồi tab lần trước trên trình duyệt này. */
    1416function initialTab() {
     17  const fromUrl = new URLSearchParams(window.location.search).get('smb_tab');
     18  if (fromUrl && TABS.includes(fromUrl)) return fromUrl;
    1519  try {
    1620    const saved = window.localStorage.getItem(TAB_KEY);
    … …  
    2327const AdminApp = () => {
    2428  const [activeTab, setActiveTabState] = useState(initialTab);
     29  const [insightsMod, setInsightsMod] = useState<any>(null);
     30  const [insightsFailed, setInsightsFailed] = useState(false);
     31  const [inboxOpenId, setInboxOpenId] = useState<number | null>(null);
    2532  const setActiveTab = (tab: string) => {
    2633    setActiveTabState(tab);
    … …  
    2835  };
    2936  const valueTitle = (window as any).SMBCommerceAdmin?.i18n?.tab || 'AI Value & Performance';
     37  const insightsTitle = (window as any).SMBCommerceAdmin?.i18n?.insightsTab || 'Sales Insights';
     38
     39  // Sales Insights (2.6.0): module riêng, chỉ tải khi mở tab.
     40  useEffect(() => {
     41    if (activeTab !== 'insights' || insightsMod) return;
     42    setInsightsFailed(false);
     43    loadInsights().then(mod => setInsightsMod(() => mod)).catch(() => setInsightsFailed(true));
     44  }, [activeTab, insightsMod]);
     45
     46  const openConversation = (id: number) => {
     47    setInboxOpenId(id);
     48    setActiveTab('inbox');
     49  };
     50  const tab = (key: string, label: string) => (
     51    <button className={`nav-tab ${activeTab === key ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab(key)} aria-current={activeTab === key ? 'page' : undefined}>{label}</button>
     52  );
    3053
    3154  return (
    … …  
    3356      <h1>Ninhle AI Sales Assistant Admin</h1>
    3457      <h2 className="nav-tab-wrapper">
    35         <button className={`nav-tab ${activeTab === 'value' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('value')} aria-current={activeTab === 'value' ? 'page' : undefined}>{valueTitle}</button>
    36         <button className={`nav-tab ${activeTab === 'inbox' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('inbox')} aria-current={activeTab === 'inbox' ? 'page' : undefined}>Inbox</button>
    37         <button className={`nav-tab ${activeTab === 'rules' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('rules')} aria-current={activeTab === 'rules' ? 'page' : undefined}>Rules Engine</button>
    38         <button className={`nav-tab ${activeTab === 'leads' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('leads')} aria-current={activeTab === 'leads' ? 'page' : undefined}>Leads</button>
     58        {tab('value', valueTitle)}
     59        {tab('insights', insightsTitle)}
     60        {tab('inbox', 'Inbox')}
     61        {tab('rules', 'Rules Engine')}
     62        {tab('leads', 'Leads')}
    3963      </h2>
    4064      <div style={{ marginTop: 20 }}>
    4165          {activeTab === 'value' && <ValueDashboard />}
    42           {activeTab === 'inbox' && <Inbox />}
     66          {activeTab === 'insights' && (insightsMod
     67            ? <insightsMod.SalesInsights onOpenConversation={openConversation} />
     68            : insightsFailed
     69              ? <div className="notice notice-error smb-inline-notice" role="alert"><p>{t('loadError')}</p></div>
     70              : <p role="status" className="smb-value-status">{t('loading')}</p>)}
     71          {activeTab === 'inbox' && <Inbox openId={inboxOpenId} />}
    4372          {activeTab === 'rules' && <Rules />}
    4473          {activeTab === 'leads' && <Leads />}
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/style.css

    r3752682 r3765552  
    374374}
    375375
     376/* Thẻ khách trong Inbox (2.6.0, S12): cột phải; màn hẹp thì thành khối thu gọn trên khung chat. */
     377.smb-customer-aside {
     378    width: 260px;
     379    flex-shrink: 0;
     380    border-left: 1px solid #ccd0d4;
     381    overflow-y: auto;
     382    background: #fff;
     383    padding: 14px;
     384}
     385.smb-customer-inline {
     386    display: none;
     387    border-bottom: 1px solid #ccd0d4;
     388    padding: 8px 15px;
     389    background: #f6f7f7;
     390}
     391.smb-customer-inline summary {
     392    cursor: pointer;
     393    font-weight: 600;
     394}
     395.smb-customer-card h4 {
     396    margin: 0 0 8px;
     397    font-size: 14px;
     398}
     399.smb-customer-card dl {
     400    margin: 0;
     401}
     402.smb-customer-card dt {
     403    margin-top: 10px;
     404    color: #646970;
     405    font-size: 12px;
     406}
     407.smb-customer-card dd {
     408    margin: 2px 0 0;
     409    overflow-wrap: anywhere;
     410}
     411.smb-customer-card ul {
     412    margin: 0;
     413    padding-left: 16px;
     414    list-style: disc;
     415}
     416.smb-customer-sub,
     417.smb-customer-note {
     418    color: #646970;
     419    font-size: 12px;
     420}
     421.smb-customer-links {
     422    margin: 12px 0 0;
     423}
     424.smb-customer-warn {
     425    margin: 0 0 8px;
     426    padding: 6px 8px;
     427    background: #fcf9e8;
     428    border-left: 3px solid #dba617;
     429    font-size: 12px;
     430}
     431@media (max-width: 1100px) {
     432    .smb-customer-aside {
     433        display: none;
     434    }
     435    .smb-customer-inline {
     436        display: block;
     437    }
     438}
     439
    376440.smb-commerce-main-input {
    377441    padding: 15px;
    … …  
    656720    outline-offset: 2px;
    657721}
     722
     723/* Sales Insights (2.6.0, S6) */
     724.smb-insights-grid {
     725    display: grid;
     726    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
     727    gap: 16px;
     728    align-items: start;
     729}
     730
     731.smb-insights-wide {
     732    grid-column: 1 / -1;
     733}
     734
     735.smb-insights-table td {
     736    vertical-align: top;
     737}
     738
     739.smb-insights-table td:nth-child(2),
     740.smb-insights-table td:nth-child(3) {
     741    white-space: nowrap;
     742}
     743
     744.smb-insights-table td strong {
     745    overflow-wrap: anywhere;
     746}
     747
     748.smb-insights-actions {
     749    display: flex;
     750    flex-wrap: wrap;
     751    gap: 6px 12px;
     752    align-items: center;
     753    justify-content: flex-end;
     754}
     755
     756.smb-insights-tags {
     757    display: flex;
     758    flex-wrap: wrap;
     759    gap: 4px;
     760    margin-top: 4px;
     761}
     762
     763.smb-insights-tag {
     764    padding: 1px 8px;
     765    border-radius: 999px;
     766    background: #f0f0f1;
     767    color: #50575e;
     768    font-size: 11px;
     769}
     770
     771.smb-insights-draft form {
     772    display: grid;
     773    gap: 8px;
     774    max-width: 720px;
     775}
     776
     777.smb-insights-draft label {
     778    display: grid;
     779    gap: 4px;
     780    font-weight: 600;
     781}
     782
     783.smb-insights-draft fieldset {
     784    margin: 0;
     785    padding: 0;
     786    border: 0;
     787}
     788
     789.smb-insights-draft fieldset label {
     790    display: flex;
     791    font-weight: 400;
     792}
     793
     794.smb-insights-notes ul {
     795    margin: 0;
     796}
     797
     798.smb-insights-notes li {
     799    padding: 6px 0;
     800    border-top: 1px solid #f0f0f1;
     801}
     802
     803.smb-insights-list li {
     804    display: flex;
     805    justify-content: space-between;
     806    gap: 12px;
     807    padding: 6px 0;
     808    border-top: 1px solid #f0f0f1;
     809}
     810
     811.smb-insights-list li > span:first-child {
     812    min-width: 0;
     813    overflow-wrap: anywhere;
     814}
     815
     816.smb-insights-list li > span:last-child {
     817    flex-shrink: 0;
     818    text-align: right;
     819}
     820
     821.smb-insights-list a,
     822.smb-insights-list .button-link,
     823.smb-insights-actions .button-link {
     824    white-space: nowrap;
     825}
     826
     827.smb-insights-why {
     828    margin: 0;
     829}
     830
     831.smb-insights-why li {
     832    padding: 8px 0;
     833    border-top: 1px solid #f0f0f1;
     834}
     835
     836.smb-insights-why-row {
     837    display: flex;
     838    justify-content: space-between;
     839    gap: 12px;
     840}
     841
     842.smb-insights-bar {
     843    height: 6px;
     844    margin-top: 4px;
     845    border-radius: 999px;
     846    background: #f0f0f1;
     847    overflow: hidden;
     848}
     849
     850.smb-insights-bar span {
     851    display: block;
     852    height: 100%;
     853    background: #2271b1;
     854}
     855
     856.smb-insights-quote {
     857    margin: 4px 0 0;
     858    padding-left: 8px;
     859    border-left: 3px solid #dcdcde;
     860    color: #50575e;
     861    font-size: 12px;
     862    overflow-wrap: anywhere;
     863}
     864
     865@media (max-width: 782px) {
     866    .smb-insights-table thead {
     867        display: none;
     868    }
     869    .smb-insights-table tr,
     870    .smb-insights-table td {
     871        display: block;
     872        width: auto;
     873    }
     874    .smb-insights-table td[data-label]::before {
     875        content: attr(data-label) ": ";
     876        color: #646970;
     877    }
     878    .smb-insights-table td.smb-insights-actions {
     879        display: flex;
     880        justify-content: flex-start;
     881    }
     882    .smb-insights-list li > span:last-child {
     883        flex-shrink: 1;
     884    }
     885}
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/api.ts

    r3752682 r3765552  
    4040    },
    4141
     42    /**
     43     * Gửi tin. Khách đã đăng nhập (2.6.0, S5): kèm nonce REST để máy chủ biết tài khoản ("đơn của tôi" không
     44     * cần mã đơn). Nonce bị từ chối (trang cache cũ, phiên đăng nhập đã đổi) thì gửi lại như khách vãng lai.
     45     */
    4246    async sendMessage(conversationId: number, content: string, clientMessageId: string) {
    43         const res = await fetch(`${BASE_URL}/chat/message`, {
     47        const body = JSON.stringify({
     48            conversation_id: conversationId,
     49            content: content,
     50            client_message_id: clientMessageId
     51        });
     52        const w = window.SMBCommerceWidget;
     53        const send = (withNonce: boolean) => fetch(`${BASE_URL}/chat/message`, {
    4454            method: 'POST',
    45             headers: this.getHeaders(),
    46             body: JSON.stringify({
    47                 conversation_id: conversationId,
    48                 content: content,
    49                 client_message_id: clientMessageId
    50             })
     55            headers: withNonce ? { ...this.getHeaders(), 'X-WP-Nonce': String(w.nonce) } : this.getHeaders(),
     56            body
    5157        });
     58        const withNonce = !!(w && w.loggedIn && w.nonce);
     59        let res = await send(withNonce);
     60        if (withNonce && res.status === 403) {
     61            const err = await res.clone().json().catch(() => ({} as any));
     62            if (err && err.code === 'rest_cookie_invalid_nonce') res = await send(false);
     63        }
    5264        if (!res.ok) throw new Error('Failed to send message');
    5365        return await res.json();
    … …  
    6981     * request vẫn đi khi khách vừa bấm link rời trang; lỗi bỏ qua vì không ảnh hưởng tới chat.
    7082     */
    71     event(conversationId: number, type: string, ref = '') {
     83    event(conversationId: number, type: string, ref = '', extra: Record<string, number> = {}) {
    7284        fetch(`${BASE_URL}/chat/event`, {
    7385            method: 'POST',
    7486            headers: this.getHeaders(),
    75             body: JSON.stringify({ conversation_id: conversationId, type, ref }),
     87            body: JSON.stringify({ conversation_id: conversationId, type, ref, ...extra }),
    7688            keepalive: true
    7789        }).catch(() => { /* đo lường không được làm hỏng chat */ });
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/components/ChatButton.tsx

    r3761175 r3765552  
    88    config: any;
    99    buttonRef?: Ref<HTMLButtonElement>;
     10    /** Số tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */
     11    unread?: number;
    1012}
    1113
    12 export function ChatButton({ onClick, onIntent, config, buttonRef }: ChatButtonProps) {
     14export function ChatButton({ onClick, onIntent, config, buttonRef, unread = 0 }: ChatButtonProps) {
    1315    return (
    1416        <button
    … …  
    2022            onTouchStart={onIntent}
    2123            onFocus={onIntent}
    22             aria-label={t('openChat')}
     24            aria-label={unread > 0 ? `${t('openChat')} (${t('unreadMessages').replace('%d', String(unread))})` : t('openChat')}
    2325            aria-haspopup="dialog"
    2426        >
    … …  
    3032                </svg>
    3133            )}
     34            {unread > 0 && <span className="smb-unread" aria-hidden="true">{unread > 9 ? '9+' : unread}</span>}
    3235        </button>
    3336    );
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/components/ChatWindow.tsx

    r3761175 r3765552  
    66import { toPlainText } from '../markdown';
    77import { t } from '../i18n';
     8import { nextDelay, nextError, nextIdle, POLL } from '../pollSchedule';
    89import type { VoiceSession } from '../voice';
    910
    1011type VoiceModule = typeof import('../voice');
     12type CardsModule = typeof import('./ProductCards');
     13type CartPanelModule = typeof import('./CartPanel');
     14
     15/**
     16 * Thẻ sản phẩm (2.6.0) là chunk riêng: chỉ tải khi có tin nhắn kèm thẻ sản phẩm, thẻ đơn (S5) hoặc mã giảm giá
     17 * đề xuất (Starter).
     18 */
     19function hasCards(m: { metadata?: any }): boolean {
     20    const md = m.metadata;
     21    const some = (key: string) => Array.isArray(md[key]) && md[key].length > 0;
     22    return !!(md && (some('cards') || some('actions') || some('orders')));
     23}
    1124
    1225/** Nhập bằng giọng nói (2.5.0): chủ shop bật, trang HTTPS, trình duyệt có Web Speech API (Firefox chưa có). */
    … …  
    5063    order: 'stageOrder',
    5164    handoff: 'stageHandoff',
     65    queued: 'stageQueued',
    5266};
    5367
    … …  
    108122    const [listening, setListening] = useState(false);
    109123    const [voiceEnabled] = useState(() => voiceAvailable(config));
     124    const [cardsMod, setCardsMod] = useState<CardsModule | null>(null);
     125    // Giỏ trong chat (2.6.0, S4): chỉ khi có Store API; không có thì nút giỏ là link tới trang giỏ như trước.
     126    const store = { api: config.storeApi || '', cartUrl: config.cartUrl || '', checkoutUrl: config.checkoutUrl || '' };
     127    const [cartOpen, setCartOpen] = useState(false);
     128    const [cartPanelMod, setCartPanelMod] = useState<CartPanelModule | null>(null);
     129    const [cartCount, setCartCount] = useState<number | null>(null);
     130    const [cartRefresh, setCartRefresh] = useState(0);
    110131
    111132    const voiceModRef = useRef<VoiceModule | null>(null);
    … …  
    118139    const cancelClearRef = useRef<HTMLButtonElement>(null);
    119140    const lastMessageIdRef = useRef<number | null>(null);
    120     const pollIntervalRef = useRef<number>(5000);
     141    // Nhịp poll thích ứng (pollSchedule.ts): nhịp khi rảnh và nhịp lùi khi lỗi mạng.
     142    const idleDelayRef = useRef<number>(POLL.idleStart);
     143    const errorDelayRef = useRef<number>(0);
     144    const stageRef = useRef<string | null>(null);
     145    const statusRef = useRef<string>(initialStatus || 'bot_active');
    121146    const timeoutRef = useRef<number | null>(null);
     147    /** Lúc gửi tin đang chờ câu trả lời (0 = không chờ). */
     148    const awaitingSinceRef = useRef<number>(0);
     149    /** Ngôn ngữ khách đang viết, máy chủ nhận từ tin nhắn (2.6.0); null = chưa biết. */
     150    const languageRef = useRef<string | null>(null);
    122151
    123152    const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
    … …  
    128157
    129158    useEffect(() => { inputRef.current?.focus(); }, []);
     159
     160    useEffect(() => {
     161        if (!cardsMod && messages.some(hasCards)) {
     162            import('./ProductCards').then(mod => setCardsMod(() => mod)).catch(() => { /* mạng lỗi: vẫn còn chữ */ });
     163        }
     164    }, [messages]);
    130165
    131166    // Tải sẵn module giọng nói khi khung chat mở (chỉ khi tính năng bật và trình duyệt hỗ trợ), để lúc khách
    … …  
    181216
    182217    const applyState = (data: any) => {
    183         if (typeof data.status === 'string') setStatus(data.status);
     218        if (typeof data.status === 'string') { setStatus(data.status); statusRef.current = data.status; }
    184219        if ('agent_name' in data) setAgentName(data.agent_name || null);
    185         if ('stage' in data) setStage(data.stage || null);
     220        if ('stage' in data) { setStage(data.stage || null); stageRef.current = data.stage || null; }
     221        if (typeof data.language === 'string' && data.language) languageRef.current = data.language;
    186222    };
    187223
    188224    const poll = async () => {
    189225        if (document.visibilityState !== 'visible') {
    190             timeoutRef.current = window.setTimeout(poll, pollIntervalRef.current);
     226            // Tab ẩn: không gọi máy chủ; lượt poll chạy lại ngay khi tab hiện (visibilitychange).
     227            timeoutRef.current = window.setTimeout(poll, POLL.idleMax);
    191228            return;
    192229        }
     230        let gotMessages = false;
    193231        try {
    194232            const data = await api.pollMessages(conversationId, lastMessageIdRef.current);
    … …  
    201239                const incoming = data.messages.filter((m: any) => m.sender_type !== 'user');
    202240                if (incoming.length > 0) {
     241                    awaitingSinceRef.current = 0;
    203242                    setIsTyping(false);
    204243                    setStage(null);
    … …  
    206245                    setLiveText(toPlainText(incoming[incoming.length - 1].content).slice(0, 300));
    207246                }
    208                 pollIntervalRef.current = 5000;
     247                gotMessages = true;
    209248            }
     249            errorDelayRef.current = 0;
     250            idleDelayRef.current = nextIdle(idleDelayRef.current, gotMessages);
    210251        } catch (err) {
    211             pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000);
    212         }
    213         timeoutRef.current = window.setTimeout(poll, pollIntervalRef.current);
    214     };
     252            errorDelayRef.current = nextError(errorDelayRef.current);
     253        }
     254        timeoutRef.current = window.setTimeout(poll, nextPollDelay());
     255    };
     256
     257    const nextPollDelay = () => nextDelay({
     258        now: Date.now(),
     259        awaitingSince: awaitingSinceRef.current,
     260        stage: stageRef.current,
     261        status: statusRef.current,
     262        idleDelay: idleDelayRef.current,
     263        errorDelay: errorDelayRef.current,
     264    });
    215265
    216266    useEffect(() => {
    … …  
    219269        const handleVisibility = () => {
    220270            if (document.visibilityState === 'visible') {
    221                 pollIntervalRef.current = 5000;
     271                idleDelayRef.current = POLL.idleStart;
     272                errorDelayRef.current = 0;
    222273                if (timeoutRef.current) clearTimeout(timeoutRef.current);
    223274                poll();
    … …  
    232283
    233284    const pollSoon = () => {
    234         // Việc gọi AI được xếp hàng đợi và chính request poll là nơi chạy nó, nên poll ngay.
     285        // Câu về giỏ hàng do chính lượt poll của khách trả lời (cần cookie phiên), nên poll ngay.
    235286        if (timeoutRef.current) clearTimeout(timeoutRef.current);
    236287        timeoutRef.current = window.setTimeout(poll, 100);
    … …  
    244295    };
    245296
    246     // Ngôn ngữ nhận dạng: <html lang> của trang (P4 sẽ thay bằng ngôn ngữ hội thoại), rồi ngôn ngữ trình duyệt.
     297    // Ngôn ngữ nhận dạng: ngôn ngữ khách đang viết trong hội thoại, rồi <html lang> của trang, rồi trình duyệt.
    247298    const voiceLang = (mod: VoiceModule) => mod.pickLanguage(
    248         document.documentElement.lang,
     299        languageRef.current || document.documentElement.lang,
    249300        navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language]
    250301    );
    … …  
    300351            const savedMsg = await api.sendMessage(conversationId, content, crypto.randomUUID());
    301352            setMessages(prev => prev.map(m => m.id === tempMsg.id ? savedMsg : m));
     353            if (status === 'bot_active') awaitingSinceRef.current = Date.now();
    302354            pollSoon();
    303355        } catch (err) {
    … …  
    372424
    373425    // Bấm link trong chat: tương tác đủ điều kiện cho chỉ số đơn có AI hỗ trợ.
     426    const toggleCart = () => {
     427        if (cartOpen) { setCartOpen(false); return; }
     428        import('./CartPanel').then(mod => { setCartPanelMod(() => mod); setCartOpen(true); }).catch(() => {
     429            // Không tải được chunk: mở trang giỏ của WooCommerce như trước.
     430            if (store.cartUrl) window.location.href = store.cartUrl;
     431        });
     432    };
     433
     434    const onCartAdd = (card: { id: number }, itemsCount: number, quantity = 1) => {
     435        setCartCount(itemsCount);
     436        setCartRefresh(n => n + 1);
     437        api.event(conversationId, 'cart_add', String(card.id), { qty: quantity });
     438    };
     439
    374440    const trackLink = (url: string) => {
    375441        try {
    … …  
    445511                </div>
    446512                <div className="smb-chat-actions">
    447                     {config.cartUrl && (
     513                    {config.cartUrl && (store.api ? (
     514                        <button type="button" className="smb-icon-btn smb-cart-btn" onClick={toggleCart} aria-expanded={cartOpen}
     515                            aria-controls="smb-cart-panel" aria-label={t('cart')} title={t('cart')}>
     516                            <Icon d={ICONS.cart} />
     517                            {cartCount ? <span className="smb-cart-badge" aria-hidden="true">{cartCount > 99 ? '99+' : cartCount}</span> : null}
     518                        </button>
     519                    ) : (
    448520                        <a className="smb-icon-btn" href={config.cartUrl} aria-label={t('cart')} title={t('cart')}><Icon d={ICONS.cart} /></a>
    449                     )}
     521                    ))}
    450522                    <button type="button" className="smb-icon-btn smb-expand-btn" onClick={() => setExpanded(!expanded)}
    451523                        aria-pressed={expanded} aria-label={expanded ? t('collapse') : t('expand')} title={expanded ? t('collapse') : t('expand')}>
    … …  
    471543
    472544            <div className="smb-chat-body">
     545                {cartOpen && cartPanelMod && (
     546                    <cartPanelMod.CartPanel store={store} refresh={cartRefresh} onClose={() => setCartOpen(false)} onCount={setCartCount}
     547                        proVersion={config.proModule || ''} />
     548                )}
    473549                {config.aiNotice && <p className="smb-ai-notice">{config.aiNotice}</p>}
    474550
    … …  
    484560                )}
    485561
    486                 {messages.map(m => (
     562                {messages.map(m => [
    487563                    <div key={m.id} className={`smb-msg ${m.sender_type === 'user' ? 'is-user' : m.sender_type === 'agent' ? 'is-agent' : 'is-bot'}`}>
    488564                        <div className="smb-bubble"><MessageContent text={m.content} rich={m.sender_type !== 'user'} onLinkClick={trackLink} /></div>
    489                     </div>
    490                 ))}
     565                    </div>,
     566                    cardsMod && m.sender_type !== 'user' && hasCards(m) ? (
     567                        <div key={`${m.id}-cards`} className="smb-msg is-bot smb-msg-cards">
     568                            <cardsMod.ProductCards metadata={m.metadata} onView={trackLink} store={store} onCartAdd={onCartAdd}
     569                                proVersion={config.proModule || ''} onCouponApplied={() => setCartRefresh(n => n + 1)} />
     570                            <cardsMod.OrderCards metadata={m.metadata} onView={trackLink} />
     571                        </div>
     572                    ) : null,
     573                ])}
    491574
    492575                {isTyping && (
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/i18n.ts

    r3761175 r3765552  
    1111const FALLBACK: Strings = {
    1212    openChat: 'Open chat',
     13    unreadMessages: 'new replies: %d',
    1314    chatWindow: 'Chat with our assistant',
    1415    cart: 'Cart',
    … …  
    5051    stageOrder: 'Looking up your order…',
    5152    stageHandoff: 'Connecting you with a team member…',
     53    stageQueued: 'Waiting in line…',
     54    productsLabel: 'Products',
     55    cardsPrev: 'Previous products',
     56    cardsNext: 'Next products',
     57    cardView: 'View',
     58    stockIn: 'In stock',
     59    stockOut: 'Out of stock',
     60    stockBackorder: 'On backorder',
     61    priceAt: 'Price on %s',
     62    addToCart: 'Add to cart',
     63    adding: 'Adding…',
     64    chooseOptions: 'Choose options',
     65    addedToCart: 'Added to cart',
     66    viewCart: 'View cart',
     67    checkout: 'Checkout',
     68    cartOutOfStock: 'Sorry, this item just sold out.',
     69    cartLimit: "You can't add more of this item.",
     70    cartError: "Couldn't add to cart.",
     71    openProduct: 'Open the product page',
     72    yourCart: 'Your cart',
     73    closeCart: 'Close',
     74    loadingCart: 'Loading your cart…',
     75    cartLoadError: "Couldn't load your cart.",
     76    cartEmpty: 'Your cart is empty.',
     77    subtotal: 'Subtotal',
     78    discount: 'Discount',
     79    ordersLabel: 'Your orders',
     80    orderTitle: 'Order #%s',
     81    orderMore: 'and %d more',
     82    orderTracking: 'Tracking number',
     83    orderTrack: 'Track',
     84    orderView: 'View order',
    5285    resolutionQuestion: 'Did this answer your question?',
    5386    yes: 'Yes',
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/index.tsx

    r3761175 r3765552  
    44import { api } from './api';
    55import { onColor } from './color';
     6import { PEEK_MS, countUnread, lastId, mergeMessages, readSeen, shouldPeek, writeSeen } from './peek';
    67import './style.css';
    78
    … …  
    4142    const wasOpen = useRef(false);
    4243    const [chatModule, setChatModule] = useState<ChatWindowModule | null>(null);
     44    // Tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9): peek.ts quyết định khi nào hỏi máy chủ.
     45    const [unread, setUnread] = useState(0);
     46    const seenRef = useRef({ id: 0, at: 0 });
     47    const live = useRef({ isOpen, conversationId, status, messages });
     48    live.current = { isOpen, conversationId, status, messages };
     49
     50    useEffect(() => {
     51        if (conversationId === null) return;
     52        const seen = readSeen(conversationId);
     53        // Lần đầu trên trình duyệt này: coi mọi tin đã có là đã thấy.
     54        seenRef.current = seen.id > 0 ? seen : { id: lastId(messages), at: Date.now() };
     55        if (seen.id === 0) writeSeen(conversationId, seenRef.current.id, seenRef.current.at);
     56        setUnread(countUnread(messages, seenRef.current.id));
     57    }, [conversationId]);
     58
     59    useEffect(() => {
     60        if (conversationId === null) return;
     61        // Đang mở (mọi tin hiện ra là đã thấy) hoặc vừa thu nhỏ (hội thoại vừa có hoạt động).
     62        // Ghi cả lúc đang mở, vì khách có thể đóng tab mà không thu nhỏ khung chat.
     63        if (isOpen || wasOpen.current) {
     64            seenRef.current = { id: lastId(messages), at: Date.now() };
     65            writeSeen(conversationId, seenRef.current.id, seenRef.current.at);
     66            setUnread(0);
     67        }
     68    }, [isOpen, messages]);
     69
     70    useEffect(() => {
     71        const timer = window.setInterval(async () => {
     72            const s = live.current;
     73            if (!shouldPeek({ open: s.isOpen, conversationId: s.conversationId, status: s.status,
     74                lastActivityAt: seenRef.current.at, visible: document.visibilityState === 'visible', now: Date.now() })) return;
     75            try {
     76                const data = await api.pollMessages(s.conversationId as number, lastId(s.messages) || null);
     77                if (data.status) setStatus(data.status);
     78                const incoming = (data.messages || []).filter((m: any) => Number(m.id) > lastId(live.current.messages));
     79                if (!incoming.length) return;
     80                const merged = mergeMessages(live.current.messages, incoming);
     81                setMessages(merged);
     82                seenRef.current = { ...seenRef.current, at: Date.now() };
     83                writeSeen(s.conversationId as number, seenRef.current.id, seenRef.current.at);
     84                setUnread(countUnread(merged, seenRef.current.id));
     85            } catch { /* mạng lỗi: thử lại ở nhịp sau */ }
     86        }, PEEK_MS);
     87        return () => window.clearInterval(timer);
     88    }, []);
    4389
    4490    const prefetchChat = () => {
    … …  
    232278                />
    233279            ) : (
    234                 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} />
     280                <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} unread={unread} />
    235281            )}
    236282        </div>
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/style.css

    r3761175 r3765552  
    9191
    9292.smb-commerce-widget-container .smb-launcher:hover { transform: scale(1.06); }
    93 .smb-commerce-widget-container .smb-launcher.has-logo { overflow: hidden; }
     93/* Ảnh logo tự bo tròn (luật img bên dưới), nên nút không cắt tràn: huy hiệu tin chưa đọc nằm ngoài vòng tròn. */
     94.smb-commerce-widget-container .smb-launcher { position: relative; }
     95/* Tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */
     96.smb-commerce-widget-container .smb-unread {
     97  position: absolute;
     98  top: -4px;
     99  right: -4px;
     100  min-width: 22px;
     101  height: 22px;
     102  padding: 0 6px;
     103  box-sizing: border-box;
     104  border-radius: 11px;
     105  background: #d63638;
     106  color: #fff;
     107  border: 2px solid #fff;
     108  font-size: 12px;
     109  font-weight: 600;
     110  line-height: 18px;
     111  text-align: center;
     112}
    94113.smb-launcher img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
    95114
    … …  
    247266.is-bot .smb-bubble { background: var(--smb-bg-soft); color: var(--smb-text); border-bottom-left-radius: 6px; }
    248267.is-agent .smb-bubble { background: #e8efff; color: var(--smb-text); border-bottom-left-radius: 6px; }
     268
     269/* Thẻ sản phẩm (2.6.0, S3): 2–3 thẻ cuộn ngang dưới câu trả lời. */
     270.smb-msg.smb-msg-cards { max-width: 100%; width: 100%; margin-top: -2px; flex-direction: column; }
     271.smb-cards { position: relative; width: 100%; }
     272.smb-cards-track {
     273  display: flex; gap: 8px; margin: 0; padding: 2px 2px 6px; list-style: none;
     274  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch;
     275}
     276.smb-cards-track::-webkit-scrollbar { display: none; }
     277.smb-pcard {
     278  flex: 0 0 82%; max-width: 260px; scroll-snap-align: start;
     279  display: flex; gap: 10px; align-items: flex-start; padding: 10px;
     280  border: 1px solid rgba(15, 23, 42, .1); border-radius: 14px; background: var(--smb-bg); color: var(--smb-text);
     281}
     282.smb-cards.is-single .smb-pcard { flex-basis: 100%; max-width: 100%; }
     283.smb-pcard-img { flex: 0 0 64px; width: 64px; height: 64px; border-radius: 10px; object-fit: cover; background: var(--smb-bg-soft); }
     284.smb-pcard-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; font-size: 13px; line-height: 1.35; }
     285.smb-commerce-widget-container .smb-pcard-name {
     286  color: var(--smb-text); font-weight: 600; text-decoration: none;
     287  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
     288}
     289.smb-commerce-widget-container .smb-pcard-name:hover { text-decoration: underline; }
     290.smb-pcard-price { margin: 0; font-weight: 600; }
     291.smb-pcard-price del { margin-right: 6px; color: var(--smb-text-soft); font-weight: 400; }
     292.smb-pcard-stock { margin: 0; font-size: 12px; color: #15803d; }
     293.smb-pcard-stock.is-outofstock { color: #b42318; }
     294.smb-pcard-stock.is-onbackorder { color: #b45309; }
     295.smb-pcard-at { margin: 0; font-size: 11px; color: var(--smb-text-soft); }
     296.smb-pcard-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
     297/* Tiền tố container: thắng reset ".smb-commerce-widget-container a" (bỏ viền, nền, padding). */
     298.smb-commerce-widget-container .smb-pcard-btn {
     299  display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border-radius: 999px;
     300  border: 1px solid var(--smb-primary); color: var(--smb-primary); background: transparent;
     301  font-size: 13px; font-weight: 600; text-decoration: none; cursor: pointer;
     302}
     303.smb-commerce-widget-container .smb-pcard-btn:hover { background: var(--smb-primary); color: var(--smb-on-primary); }
     304.smb-commerce-widget-container .smb-cards-nav {
     305  position: absolute; top: 50%; transform: translateY(-50%); z-index: 1;
     306  width: 30px; height: 30px; border-radius: 50%; border: 0; cursor: pointer;
     307  background: var(--smb-bg); color: var(--smb-text); border: 1px solid rgba(15, 23, 42, .12);
     308  box-shadow: 0 2px 8px rgba(15, 23, 42, .18); font-size: 20px; font-weight: 600; line-height: 1;
     309}
     310.smb-commerce-widget-container .smb-cards-nav.is-prev { left: -4px; }
     311.smb-commerce-widget-container .smb-cards-nav.is-next { right: -4px; }
     312.smb-pcard-btn:focus-visible, .smb-cards-nav:focus-visible, .smb-pcard-name:focus-visible { outline: 2px solid var(--smb-focus); outline-offset: 2px; }
     313@media (hover: none) { .smb-commerce-widget-container .smb-cards-nav { display: none; } }
     314
     315/* Giỏ hàng trong chat (2.6.0, S4). */
     316.smb-commerce-widget-container .smb-pcard-btn.is-primary { background: var(--smb-primary); color: var(--smb-on-primary); }
     317.smb-commerce-widget-container .smb-pcard-btn.is-primary:hover { filter: brightness(1.08); }
     318.smb-commerce-widget-container .smb-pcard-btn[disabled] { opacity: .7; cursor: progress; }
     319.smb-commerce-widget-container .smb-pcard-link { color: var(--smb-primary); text-decoration: underline; font-size: 13px; }
     320.smb-pcard-added { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
     321.smb-pcard-added-text { color: #15803d; font-weight: 600; font-size: 13px; }
     322.smb-pcard-error { flex-basis: 100%; color: #b42318; font-size: 12px; }
     323.smb-commerce-widget-container .smb-cart-btn { position: relative; }
     324.smb-cart-badge {
     325  position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
     326  background: #fff; color: var(--smb-primary); font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
     327}
     328.smb-cart-panel {
     329  margin: 0 0 8px; padding: 12px 14px; border-radius: 16px; background: var(--smb-bg);
     330  border: 1px solid rgba(15, 23, 42, .12); box-shadow: 0 6px 18px rgba(15, 23, 42, .12);
     331}
     332.smb-cart-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
     333.smb-cart-head h3 { margin: 0; font-size: 15px; font-weight: 700; color: var(--smb-text); }
     334.smb-cart-head h3:focus { outline: none; }
     335.smb-cart-note { margin: 4px 0; font-size: 13px; color: var(--smb-text-soft); }
     336.smb-cart-items { list-style: none; margin: 0; padding: 0; }
     337.smb-cart-item { display: flex; gap: 8px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid rgba(15, 23, 42, .08); font-size: 13px; }
     338.smb-cart-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
     339.smb-cart-qty { color: var(--smb-text-soft); white-space: nowrap; }
     340.smb-cart-line { font-weight: 600; white-space: nowrap; }
     341.smb-cart-subtotal { display: flex; justify-content: space-between; margin: 8px 0; font-size: 14px; }
     342.smb-cart-options { display: block; font-size: 12px; color: var(--smb-text-soft); }
     343
     344/* Thẻ đơn hàng (2.6.0, S5). */
     345.smb-ocards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; width: 100%; }
     346.smb-ocard {
     347  padding: 10px 12px; border: 1px solid rgba(15, 23, 42, .1); border-radius: 14px;
     348  background: var(--smb-bg); color: var(--smb-text); font-size: 13px; line-height: 1.4;
     349}
     350.smb-ocard-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 8px; }
     351.smb-ocard-status { padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; background: var(--smb-bg-soft); color: var(--smb-text); }
     352.smb-ocard-status.is-processing, .smb-ocard-status.is-on-hold { background: #fef3c7; color: #92400e; }
     353.smb-ocard-status.is-completed { background: #dcfce7; color: #166534; }
     354.smb-ocard-status.is-cancelled, .smb-ocard-status.is-failed, .smb-ocard-status.is-refunded { background: #fee2e2; color: #991b1b; }
     355.smb-ocard-meta { margin: 4px 0 0; color: var(--smb-text-soft); font-size: 12px; }
     356.smb-commerce-widget-container .smb-ocard-items { list-style: none; margin: 6px 0 0; padding: 0; }
     357.smb-ocard-items li { overflow-wrap: anywhere; }
     358.smb-ocard-more { color: var(--smb-text-soft); }
     359.smb-ocard-track { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; margin: 8px 0 0; }
     360.smb-ocard-track code { font-size: 12px; overflow-wrap: anywhere; }
     361.smb-ocard .smb-pcard-link { display: inline-block; margin-top: 6px; }
     362.smb-cart-discount { display: flex; justify-content: space-between; margin: -4px 0 8px; font-size: 13px; color: #15803d; }
     363.smb-cart-actions { display: flex; gap: 8px; flex-wrap: wrap; }
    249364
    250365.smb-commerce-widget-container .message-link { color: #1d4ed8; text-decoration: underline; overflow-wrap: anywhere; }
  • ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/voice.ts

    r3761175 r3765552  
    2525    vi: 'vi-VN', en: 'en-US', th: 'th-TH', id: 'id-ID', ms: 'ms-MY', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN',
    2626    fr: 'fr-FR', de: 'de-DE', es: 'es-ES', pt: 'pt-BR', it: 'it-IT', ru: 'ru-RU', nl: 'nl-NL', pl: 'pl-PL',
    27     tr: 'tr-TR', hi: 'hi-IN', ar: 'ar-SA', fil: 'fil-PH', km: 'km-KH', lo: 'lo-LA',
     27    tr: 'tr-TR', hi: 'hi-IN', ar: 'ar-SA', fil: 'fil-PH', km: 'km-KH', lo: 'lo-LA', uk: 'uk-UA',
    2828};
    2929
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/AIProviderFactory.php

    r3747521 r3765552  
    1313
    1414    /** Khoá setting chứa API key tương ứng mỗi provider. */
    15     private static function key_setting($provider) {
     15    public static function key_setting($provider) {
    1616        $map = [
    1717            'openai' => 'smbcommerce_openai_key',
    … …  
    2020        ];
    2121        return $map[$provider] ?? '';
     22    }
     23
     24    /** Provider dùng được: có khoá API trong plugin, hoặc (WordPress) có nhà cung cấp trong Connectors. */
     25    public static function is_configured($provider) {
     26        if ($provider === Providers\WordPressAiProvider::NAME) {
     27            return Providers\WordPressAiProvider::available();
     28        }
     29        $key_setting = self::key_setting($provider);
     30        return $key_setting !== '' && !empty(SettingsManager::get_setting($key_setting));
    2231    }
    2332
    … …  
    3746                if (class_exists('\\Smartbuy\SMBCommerce\\AI\\Providers\\GeminiProvider')) {
    3847                    return new Providers\GeminiProvider();
     48                }
     49                break;
     50            // Kết nối AI của WordPress 7.0+ (2.6.0, S7): khoá nằm trong Settings → Connectors của WordPress.
     51            case Providers\WordPressAiProvider::NAME:
     52                if (Providers\WordPressAiProvider::available()) {
     53                    return new Providers\WordPressAiProvider();
    3954                }
    4055                break;
    … …  
    6580        $chain = [];
    6681        foreach ($order as $name) {
    67             $key_setting = self::key_setting($name);
    68             if ($key_setting === '' || empty(SettingsManager::get_setting($key_setting))) {
     82            if (!self::is_configured($name)) {
    6983                continue;
    7084            }
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/AIService.php

    r3752682 r3765552  
    8787            $user_context .= "No user info provided yet. Do not assume their name.\n";
    8888        }
    89         $user_context .= "- Order history: unknown. Never invent orders. To look one up, call the get_order_status tool after the customer provides an order code or email.\n";
     89        if (\Smartbuy\SMBCommerce\Support\CustomerContext::user_id() > 0 && class_exists('WooCommerce')) {
     90            // S5: khách đăng nhập lúc gửi tin; tài khoản là bằng chứng sở hữu đơn.
     91            $user_context .= "- The customer is logged in to their store account. For questions about their orders or deliveries, call get_my_orders first; do not ask for an order number, email or phone. Never invent orders.\n";
     92        } else {
     93            $user_context .= "- Order history: unknown. Never invent orders. To look one up, call the get_order_status tool after the customer provides an order code or email.\n";
     94        }
    9095
    9196        // Tin nhắn mới nhất của khách: dùng làm từ khoá truy vấn catalog và trích nội dung link/file.
    … …  
    100105        // WooCommerce Product RAG Context
    101106        $product_context = '';
     107        $context_ids = [];
    102108        if (class_exists('\\Smartbuy\SMBCommerce\\WooCommerce\\WooCommerceProductScanner')) {
    103109            $scanner = new \Smartbuy\SMBCommerce\WooCommerce\WooCommerceProductScanner();
    … …  
    105111                $latest_user_message !== '' ? [$latest_user_message] : []
    106112            );
     113            $context_ids = $scanner->context_ids();
    107114        }
    108115
    … …  
    123130        $sales_playbook = SalesPlaybook::build($playbook_state);
    124131
    125         $capability_limits = self::capability_limits();
     132        // Starter (S4): đặt sẵn sản phẩm khách đã chọn, mã giảm giá chủ shop cho phép; '' ở bản Free.
     133        $woo = class_exists('WooCommerce');
     134        $cart_actions_prompt = $woo ? \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::prompt_block() : '';
     135        $capability_limits = self::capability_limits($woo && \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::offers_coupons());
    126136
    127137        // Kịch bản đứng đầu để định hình vai trò, sau đó mới tới ràng buộc và dữ liệu.
    … …  
    130140            $guardrails,
    131141            $capability_limits,
     142            $cart_actions_prompt,
    132143            $master_prompt,
    133144            $handoff_instructions,
    134145            $suggestion_instructions,
     146            // Thẻ sản phẩm (2.6.0, S3): chỉ có nghĩa khi có cửa hàng WooCommerce.
     147            class_exists('WooCommerce') ? \Smartbuy\SMBCommerce\Commerce\CardBuilder::PROMPT : '',
     148            // Sales Insights (2.6.0, S6): model đánh dấu câu nó không trả lời được từ dữ liệu của shop.
     149            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::PROMPT,
    135150            $user_context,
    136151            $history_digest,
    … …  
    152167                $final_system_prompt .= "\n\n[SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES]\n" . $extracted_content;
    153168            }
     169        }
     170
     171        // Ngôn ngữ trả lời (S2): khối đặt CUỐI, sau phần cố định để prompt cache của nhà cung cấp vẫn
     172        // trúng; nằm trong khoá cache câu trả lời bên dưới nên mỗi ngôn ngữ có câu trả lời riêng.
     173        $language_block = ReplyLanguage::prompt_block($conversation['language'] ?? null);
     174        if ($language_block !== '') {
     175            $final_system_prompt .= "\n\n" . $language_block;
    154176        }
    155177
    … …  
    187209
    188210        if (\Smartbuy\SMBCommerce\Commerce\LocalResolver::is_local($routing)) {
     211            // Câu trả lời tại chỗ về đơn hàng cũng có thẻ đơn (S5): LocalHandler ghi đơn đã kiểm vào sổ.
     212            ToolResultLedger::reset();
    189213            $local_reply = \Smartbuy\SMBCommerce\Commerce\LocalHandler::handle($routing);
    190214
    191215            if ($local_reply !== null && $local_reply !== '') {
    192                 MessageRepository::add_message($conversation_id, 'system', $local_reply);
     216                $local_orders = \Smartbuy\SMBCommerce\Commerce\CardBuilder::order_cards($local_reply);
     217                \Smartbuy\SMBCommerce\Commerce\CustomerProof::from_ledger($conversation_id);
     218                MessageRepository::add_message($conversation_id, 'system', $local_reply, null, $local_orders ? ['orders' => $local_orders] : null);
    193219                self::log_ai_request($conversation_id, 'chat', 0, 0, 0, 'local', false, $routing['reason']);
    194220                return true;
    … …  
    237263                \Smartbuy\SMBCommerce\Commerce\RoutingReason::UNRESOLVED_LOCAL_ONLY
    238264            );
     265            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'local_only');
    239266            return true;
    240267        }
    … …  
    318345        $rounds = 0;
    319346        $tool_handoff_priority = null;
     347        // Model chuyển nhân viên vì thiếu thông tin (create_handoff cause=missing_information): câu chưa trả lời được.
     348        $handoff_missing_info = false;
    320349        // Gom kết quả tool để đối chiếu số liệu trong câu trả lời cuối.
    321350        $grounding_from_tools = '';
    … …  
    329358            'contact_id' => (int) ($conversation['contact_id'] ?? 0),
    330359        ]);
     360        // Sản phẩm tool trả về trong lượt này, cùng sản phẩm thật trong [PRODUCT CATALOG] của lượt: chỉ chúng mới
     361        // được hiện thành thẻ hoặc đề xuất vào giỏ.
     362        ToolResultLedger::reset();
     363        ToolResultLedger::know($context_ids);
    331364
    332365        while (!empty($tool_calls) && $rounds < self::MAX_TOOL_ROUNDS) {
    … …  
    341374                self::set_stage($conversation_id, self::STAGE_BY_TOOL[$tool_name] ?? 'thinking');
    342375                $result = ToolExecutor::execute_tool($tool_name, $tool_params);
     376                ToolResultLedger::record($tool_name, $result);
     377                \Smartbuy\SMBCommerce\Insights\InsightsRecorder::tool_result($conversation_id, $tool_name, $tool_params, $result);
    343378
    344379                // Gọi tool handoff nghĩa là thật sự phải chuyển cho người thật,
    … …  
    346381                if ($tool_name === 'create_handoff' && !empty($result['success'])) {
    347382                    $tool_handoff_priority = $tool_params['priority'] ?? 'normal';
     383                    $handoff_missing_info = ($tool_params['cause'] ?? '') === 'missing_information';
    348384                }
    349385
    … …  
    369405            // Provider tự dựng message theo định dạng của mình.
    370406            $provider->append_tool_turn($messages_context, $assistant_message, $tool_results);
     407
     408            // Lượt chạy nền gần hết ngân sách (TurnClock): đừng mở thêm lời gọi chắc chắn bị cắt giữa chừng.
     409            $left = TurnClock::remaining();
     410            if ($left !== null && $left < 15) {
     411                \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant: tool round ' . $rounds . ' skipped, the background turn is out of time.');
     412                break;
     413            }
    371414
    372415            $call_start = microtime(true);
    … …  
    396439        $reply_content = self::clean_reply($raw_reply);
    397440
     441        // Thẻ sản phẩm (S3): đọc và bỏ dấu [[show:…]] TRƯỚC khối gợi ý, vì khối gợi ý thiếu thẻ đóng
     442        // sẽ nuốt mọi thứ phía sau nó.
     443        $card_result = \Smartbuy\SMBCommerce\Commerce\CardBuilder::from_reply($reply_content);
     444        $reply_content = $card_result['text'];
     445        // Sales Insights (S6): dấu [[unknown]] = câu này không trả lời được từ dữ liệu của shop.
     446        $unknown = \Smartbuy\SMBCommerce\Insights\InsightsRecorder::strip_unknown($reply_content);
     447        $reply_content = $unknown['text'];
     448        // Starter (S4): thẻ đề xuất kèm lựa chọn sẵn, và mã giảm giá có nút áp.
     449        $cards = ToolResultLedger::merge_action_cards($card_result['cards']);
     450        $cart_actions = ToolResultLedger::actions();
     451
    398452        // Hết vòng mà model vẫn đòi gọi tool: đừng trả về câu rỗng cho khách.
    399453        if (!empty($tool_calls) && trim($reply_content) === '') {
    400             $reply_content = __('I am still looking this up for you, one moment please.', 'ninhle-ai-sales-assistant');
     454            // Không còn gì chạy tiếp sau câu này: đừng hứa "một lát nữa" (câu cũ làm khách chờ mãi, staging 25/10).
     455            $reply_content = __('Sorry, I could not finish looking that up. Please ask me again in a moment.', 'ninhle-ai-sales-assistant');
    401456        }
    402457
    … …  
    450505            }
    451506        }
     507        if (!empty($cards)) {
     508            $metadata = (array) $metadata + ['cards' => $cards];
     509        }
     510        if (!empty($cart_actions)) {
     511            $metadata = (array) $metadata + ['actions' => $cart_actions];
     512        }
     513        // Thẻ đơn (S5): đơn đã qua kiểm sở hữu trong lượt, chọn theo câu trả lời.
     514        $order_cards = \Smartbuy\SMBCommerce\Commerce\CardBuilder::order_cards($reply_content);
     515        \Smartbuy\SMBCommerce\Commerce\CustomerProof::from_ledger($conversation_id);
     516        if (!empty($order_cards)) {
     517            $metadata = (array) $metadata + ['orders' => $order_cards];
     518        }
    452519
    453520        // --- Đối chiếu mọi con số tiền với dữ liệu thật đã đưa cho model ---
    … …  
    474541        $saved_reply = MessageRepository::add_message($conversation_id, 'system', $reply_content, null, $metadata);
    475542        self::record_reply_events($conversation_id, $saved_reply, $products_shown, $product_lookups);
     543        // Sales Insights (S6): một tin của khách chỉ tính một lần; lý do đầu tiên khớp được ghi.
     544        if ($unknown['unknown']) {
     545            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'unknown');
     546        } elseif ($product_lookups > 0 && $products_shown === 0) {
     547            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'no_data');
     548        } elseif ($handoff_missing_info) {
     549            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'handoff');
     550        }
    476551
    477552        // Chỉ cache khi lượt này không gọi tool nào — kết quả tool là dữ liệu sống.
    … …  
    535610        'get_product_variations' => 'products',
    536611        'get_order_status' => 'order',
     612        'get_my_orders' => 'order',
    537613        'get_customer_downloads' => 'order',
    538614        'create_handoff' => 'handoff',
    … …  
    679755     * HỢP LỆ, bot trả lời "mã không hợp lệ". Trả về '' khi gói đã có đủ công cụ.
    680756     */
    681     private static function capability_limits() {
     757    private static function capability_limits($has_coupon_offers = false) {
    682758        $gate = '\\Smartbuy\\SMBCommerce\\Licensing\\LicenseGate';
    683759        $limits = [];
    684760        if (!$gate::can('coupon_tools')) {
    685             $limits[] = '- You cannot check coupon or discount codes. Never say a code is valid, invalid, expired or how much it saves. Tell the customer to enter the code in the cart or at checkout, where the store applies it automatically.';
     761            $limits[] = '- You cannot check coupon or discount codes. Never say a code is valid, invalid, expired or how much it saves. Tell the customer to enter the code in the cart or at checkout, where the store applies it automatically.'
     762                . ($has_coupon_offers ? ' The only exception is the store-approved codes under [COUPONS YOU MAY OFFER].' : '');
    686763        }
    687764        if (!$gate::can('product_variations')) {
    … …  
    726803     *                    và retry_count; rỗng với lượt không gọi AI (local, cache, local_only).
    727804     */
    728     private static function log_ai_request($conversation_id, $request_type, $prompt_tokens, $completion_tokens, $latency_ms, $provider_name, $error_flag, $decision, array $call = []) {
     805    public static function log_ai_request($conversation_id, $request_type, $prompt_tokens, $completion_tokens, $latency_ms, $provider_name, $error_flag, $decision, array $call = []) {
    729806        global $wpdb;
    730807        $table = $wpdb->prefix . SMBCOMMERCE_PREFIX . 'ai_logs';
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/ClaudeProvider.php

    r3752682 r3765552  
    172172    }
    173173
     174    /**
     175     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     176     *
     177     * @param array $options model (thay model chat), max_tokens, json (chỉ là gợi ý: Anthropic theo prompt).
     178     * @return array{text: string, usage: array}|\WP_Error
     179     */
     180    public function complete($system, $user, array $options = []) {
     181        if (empty($this->api_key)) {
     182            return new \WP_Error('missing_key', 'Anthropic API Key is missing');
     183        }
     184        $model = (string) ($options['model'] ?? $this->model);
     185        $response = $this->call_api([
     186            'model' => $model,
     187            'max_tokens' => (int) ($options['max_tokens'] ?? 400),
     188            'system' => (string) $system,
     189            'messages' => [['role' => 'user', 'content' => (string) $user]],
     190        ]);
     191        if (is_wp_error($response)) {
     192            return $response;
     193        }
     194        $text = '';
     195        foreach ((array) ($response['content'] ?? []) as $block) {
     196            if (is_array($block) && ($block['type'] ?? '') === 'text') {
     197                $text .= (string) ($block['text'] ?? '');
     198            }
     199        }
     200        $cache_read = (int) ($response['usage']['cache_read_input_tokens'] ?? 0);
     201        $cache_write = (int) ($response['usage']['cache_creation_input_tokens'] ?? 0);
     202        return [
     203            'text' => $text,
     204            'usage' => [
     205                'prompt_tokens' => (int) ($response['usage']['input_tokens'] ?? 0) + $cache_read + $cache_write,
     206                'completion_tokens' => (int) ($response['usage']['output_tokens'] ?? 0),
     207                'cached_input_tokens' => $cache_read,
     208                'cache_write_tokens' => $cache_write,
     209                'model' => (string) ($response['model'] ?? $model),
     210                'reported' => isset($response['usage']),
     211            ],
     212        ];
     213    }
     214
    174215    public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) {
    175216        if (!empty($assistant_message)) {
    … …  
    199240            ],
    200241            'body'    => wp_json_encode($payload),
    201             'timeout' => 30,
     242            // 30 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     243            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(30) : 30,
    202244        ]);
    203245       
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/GeminiProvider.php

    r3752682 r3765552  
    1919    // ngừng lần lượt và khi đó mọi lượt chat đều lỗi 404 (đo trên staging 2026-10-09).
    2020    const DEFAULT_MODEL = 'gemini-flash-latest';
     21
     22    /** Token cộng thêm vào max_tokens của complete() cho phần suy nghĩ của model. */
     23    const THINKING_HEADROOM = 2048;
    2124
    2225    private $model = self::DEFAULT_MODEL;
    … …  
    116119       
    117120        $response = $this->call_api($payload);
    118        
     121
    119122        if (is_wp_error($response)) {
    120123            return $response;
    121124        }
    122        
    123         $reply = '';
    124         if (isset($response['candidates'][0]['content']['parts'][0]['text'])) {
    125             $reply = $response['candidates'][0]['content']['parts'][0]['text'];
     125
     126        $reply = self::candidate_text($response);
     127        // Lần gọi rỗng vẫn tính tiền token đầu vào: cộng vào usage nếu phải gọi lại.
     128        $spent = ['prompt' => 0, 'completion' => 0, 'cached' => 0];
     129
     130        // Candidate rỗng (không chữ, không lệnh tool): Gemini thỉnh thoảng trả như vậy và khách nhận câu "không
     131        // trả lời được" (staging 25/10). Ghi lý do để chẩn đoán, gọi lại MỘT lần nếu lượt còn thời gian.
     132        if (trim($reply) === '') {
     133            \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini returned no text (finishReason: '
     134                . (string) ($response['candidates'][0]['finishReason'] ?? ($response['promptFeedback']['blockReason'] ?? 'none')) . ').');
     135            $left = class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::remaining() : null;
     136            $blocked = in_array($response['candidates'][0]['finishReason'] ?? '', ['SAFETY', 'PROHIBITED_CONTENT', 'BLOCKLIST', 'SPII'], true)
     137                || isset($response['promptFeedback']['blockReason']);
     138            if (!$blocked && ($left === null || $left > 30)) {
     139                $first = $response['usageMetadata'] ?? [];
     140                $spent = [
     141                    'prompt' => (int) ($first['promptTokenCount'] ?? 0),
     142                    'completion' => (int) ($first['candidatesTokenCount'] ?? 0) + (int) ($first['thoughtsTokenCount'] ?? 0),
     143                    'cached' => (int) ($first['cachedContentTokenCount'] ?? 0),
     144                ];
     145                $retry = $this->call_api($payload);
     146                if (!is_wp_error($retry)) {
     147                    $response = $retry;
     148                    $reply = self::candidate_text($response);
     149                }
     150            }
    126151        }
    127152
    … …  
    136161        $meta = $response['usageMetadata'] ?? [];
    137162        $usage = [
    138             'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0),
    139             'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0),
    140             'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0),
     163            'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0) + $spent['prompt'],
     164            'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0) + $spent['completion'],
     165            'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0) + $spent['cached'],
    141166            'cache_write_tokens' => 0,
    142167            'model' => (string) ($response['modelVersion'] ?? $this->model),
    … …  
    149174            'usage' => $usage,
    150175            'assistant_message' => null, // Gemini dùng tool dạng text, không có khối riêng
     176        ];
     177    }
     178
     179    /**
     180     * Chữ của candidate đầu: ghép mọi phần có chữ, bỏ phần suy nghĩ (thought). Model đời mới có thể trả nhiều
     181     * phần, và phần đầu không nhất thiết là câu trả lời.
     182     */
     183    public static function candidate_text($response) {
     184        $text = '';
     185        foreach ((array) ($response['candidates'][0]['content']['parts'] ?? []) as $part) {
     186            if (is_array($part) && isset($part['text']) && empty($part['thought'])) {
     187                $text .= (string) $part['text'];
     188            }
     189        }
     190        return $text;
     191    }
     192
     193    /**
     194     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     195     *
     196     * @param array $options model (thay model chat), max_tokens, json (responseMimeType application/json).
     197     * @return array{text: string, usage: array}|\WP_Error
     198     */
     199    public function complete($system, $user, array $options = []) {
     200        if (empty($this->api_key)) {
     201            return new \WP_Error('missing_key', 'Gemini API Key is missing');
     202        }
     203        $chat_model = $this->model;
     204        $this->model = (string) ($options['model'] ?? $chat_model);
     205        // Model Gemini 2.5+/3.x "suy nghĩ" trước khi trả lời và token suy nghĩ tính vào maxOutputTokens: 300 token
     206        // cho một nhãn JSON từng ra câu trả lời rỗng (gemini-3.8-flash dùng ~220 token cho một câu ngắn, ninhle.com
     207        // 26/10). Chừa thêm chỗ; chi phí vẫn theo token thật đã dùng.
     208        $config = ['maxOutputTokens' => (int) ($options['max_tokens'] ?? 2048) + self::THINKING_HEADROOM];
     209        if (!empty($options['json'])) {
     210            $config['responseMimeType'] = 'application/json';
     211        }
     212        $response = $this->call_api([
     213            'systemInstruction' => ['parts' => [['text' => (string) $system]]],
     214            'contents' => [['role' => 'user', 'parts' => [['text' => (string) $user]]]],
     215            'generationConfig' => $config,
     216        ]);
     217        $model = $this->model;
     218        $this->model = $chat_model;
     219        if (is_wp_error($response)) {
     220            return $response;
     221        }
     222        $meta = $response['usageMetadata'] ?? [];
     223        return [
     224            'text' => self::candidate_text($response),
     225            'usage' => [
     226                'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0),
     227                'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0),
     228                'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0),
     229                'cache_write_tokens' => 0,
     230                'model' => (string) ($response['modelVersion'] ?? $model),
     231                'reported' => isset($response['usageMetadata']),
     232            ],
    151233        ];
    152234    }
    … …  
    188270    }
    189271
    190     private function call_api($payload) {
     272    /**
     273     * @param bool $retried Đã thử lại bằng DEFAULT_MODEL trong lượt này chưa.
     274     */
     275    private function call_api($payload, $retried = false) {
    191276        $url = $this->build_url();
    192277
    … …  
    194279            'headers' => $this->build_headers(),
    195280            'body'    => wp_json_encode($payload),
    196             'timeout' => 30, // increased from 20s
     281            // 30 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     282            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(30) : 30,
    197283        ]);
    198284       
    … …  
    213299            \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini API Error (HTTP ' . $http_code . '): '
    214300                . Redact::for_log($err_msg . ' | Body: ' . substr($body, 0, 500), [$this->api_key]));
     301            // Model đã lưu không dùng được với key này (Google đóng dần model cũ với key mới: "no longer available
     302            // to new users", ninhle.com 25–26/10; hoặc gõ sai tên): thử lại một lần bằng bí danh luôn trỏ tới bản
     303            // Flash hiện hành, để khách vẫn có câu trả lời. Chủ shop vẫn cần sửa model trong Settings.
     304            if ((int) $http_code === 404 && !$retried && $this->model !== self::DEFAULT_MODEL) {
     305                \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant: Gemini model ' . $this->model
     306                    . ' is not available to this API key; retried with ' . self::DEFAULT_MODEL . '. Change the Gemini model in Settings.');
     307                $this->model = self::DEFAULT_MODEL;
     308                return $this->call_api($payload, true);
     309            }
    215310            return new \WP_Error('gemini_error', $err_msg . ' (HTTP ' . $http_code . ')');
    216311        }
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/OpenAIProvider.php

    r3752682 r3765552  
    182182    }
    183183
     184    /**
     185     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     186     *
     187     * @param array $options model (thay model chat), max_tokens, json (response_format json_object).
     188     * @return array{text: string, usage: array}|\WP_Error
     189     */
     190    public function complete($system, $user, array $options = []) {
     191        if (empty($this->api_key)) {
     192            return new \WP_Error('missing_key', 'OpenAI API Key is missing');
     193        }
     194        $model = (string) ($options['model'] ?? $this->model);
     195        $payload = [
     196            'model' => $model,
     197            'messages' => [
     198                ['role' => 'system', 'content' => (string) $system],
     199                ['role' => 'user', 'content' => (string) $user],
     200            ],
     201            'max_completion_tokens' => (int) ($options['max_tokens'] ?? 400),
     202        ];
     203        if (!empty($options['json'])) {
     204            $payload['response_format'] = ['type' => 'json_object'];
     205        }
     206        $response = $this->call_api($payload);
     207        if (is_wp_error($response)) {
     208            return $response;
     209        }
     210        return [
     211            'text' => (string) ($response['choices'][0]['message']['content'] ?? ''),
     212            'usage' => [
     213                'prompt_tokens' => (int) ($response['usage']['prompt_tokens'] ?? 0),
     214                'completion_tokens' => (int) ($response['usage']['completion_tokens'] ?? 0),
     215                'cached_input_tokens' => (int) ($response['usage']['prompt_tokens_details']['cached_tokens'] ?? 0),
     216                'cache_write_tokens' => 0,
     217                'model' => (string) ($response['model'] ?? $model),
     218                'reported' => isset($response['usage']),
     219            ],
     220        ];
     221    }
     222
    184223    public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) {
    185224        if (!empty($assistant_message)) {
    … …  
    204243            ],
    205244            'body'    => wp_json_encode($payload),
    206             'timeout' => 15,
     245            // 15 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     246            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(15) : 15,
    207247        ]);
    208248       
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ResponseSanitizer.php

    r3752682 r3765552  
    3838        'IMPORTANT RULES',
    3939        "SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES",
     40        // 2.6.0
     41        'REPLY LANGUAGE',
     42        'PRODUCT CARDS',
    4043    ];
    4144
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/SalesPlaybook.php

    r3752682 r3765552  
    263263            'hesitating'    => self::contains($text, ['suy nghĩ', 'cân nhắc', 'để xem', 'từ từ', 'chưa cần', 'tính sau']),
    264264            'comparing'     => self::contains($text, ['so sánh', 'khác gì', 'chỗ khác', 'bên kia', 'nên chọn']),
    265             'ready_to_buy'  => self::contains($text, ['mua', 'đặt hàng', 'chốt', 'lấy', 'order', 'thanh toán']),
     265            // Tiếng Anh (2.6.0): khách nói "I would like size M, 2 of them" mà bot vẫn hỏi "dùng để làm gì?" (staging 25/10).
     266            'ready_to_buy'  => self::contains($text, ['mua', 'đặt hàng', 'chốt', 'lấy', 'order', 'thanh toán',
     267                'buy', 'add to cart', "i'll take", 'i’ll take', 'i will take', "i'd like", 'i’d like', 'i would like', 'i want']),
    266268            'asks_policy'   => self::contains($text, ['bảo hành', 'đổi trả', 'ship', 'giao hàng', 'vận chuyển', 'hoàn tiền']),
    267269        ];
    … …  
    275277        }
    276278
    277         if (!empty($s['ready_to_buy'])) {
     279        if (!empty($s['ready_to_buy']) && self::cart_actions_enabled()) {
     280            // Starter (S4): giỏ trong chat; không hỏi thêm, không xin số điện thoại để "xác nhận đơn".
     281            $hints[] = 'Khách có tín hiệu muốn mua. KHÔNG hỏi thêm câu tìm hiểu nhu cầu: tra sản phẩm (và biến thể nếu có) rồi gọi propose_cart_action với đúng lựa chọn và số lượng khách nói, ngay trong lượt này. Thiếu lựa chọn nào thì chỉ hỏi đúng lựa chọn đó.';
     282        } elseif (!empty($s['ready_to_buy'])) {
    278283            $hints[] = 'Khách có tín hiệu muốn mua. Xác nhận sản phẩm và giá, đưa link đặt hàng, và xin số điện thoại để xác nhận đơn.';
    279284        } elseif (!empty($s['price_concern'])) {
    … …  
    311316    }
    312317
     318    /** Giỏ trong chat nâng cao (Starter, S4) đang mở; bản Free: lớp Null trả false. */
     319    private static function cart_actions_enabled() {
     320        return class_exists('WooCommerce') && class_exists('\\Smartbuy\\SMBCommerce\\Commerce\\Woo\\CartActionService')
     321            && \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::enabled();
     322    }
     323
    313324    private static function contains($haystack, $needles) {
    314325        foreach ($needles as $needle) {
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ToolExecutor.php

    r3747521 r3765552  
    3838            'get_product_variations' => '/smb-commerce/v1/tools/product-variations',
    3939            'check_coupon'           => '/smb-commerce/v1/tools/check-coupon',
     40            'propose_cart_action'    => '/smb-commerce/v1/tools/propose-cart-action',
    4041            'get_order_status'       => '/smb-commerce/v1/tools/order-status',
     42            'get_my_orders'          => '/smb-commerce/v1/tools/my-orders',
    4143            'get_customer_downloads' => '/smb-commerce/v1/tools/customer-downloads',
    4244            'get_policy'             => '/smb-commerce/v1/tools/policy',
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ToolsHelper.php

    r3747521 r3765552  
    1919            'get_product_variations' => 'product_variations',
    2020            'check_coupon' => 'coupon_tools',
     21            'propose_cart_action' => 'cart_actions_advanced',
    2122        ];
    2223        $premium = \Smartbuy\SMBCommerce\Core\PremiumFallbacks::is_premium_build();
    23         $tools = array_values(array_filter($tools, function ($tool) use ($paid_tools, $premium) {
     24        // Đơn của tôi (S5): chỉ khi khách của lượt đã đăng nhập; khách vãng lai dùng get_order_status kèm bằng chứng.
     25        $logged_in = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id() > 0;
     26        $tools = array_values(array_filter($tools, function ($tool) use ($paid_tools, $premium, $logged_in) {
     27            if ($tool['name'] === 'get_my_orders') {
     28                return $logged_in;
     29            }
    2430            if (!isset($paid_tools[$tool['name']])) {
    2531                return true;
    … …  
    110116            ],
    111117            [
     118                'name' => 'propose_cart_action',
     119                'description' => 'Offer the customer a one-tap cart action shown under your reply. type "add": a product (from a tool result in this conversation) with the chosen option and quantity preselected on its card; the customer still taps Add, nothing is added by you. For products with options pass the customer\'s choice in options, e.g. {"Size": "M"} (no need to look up the variation first). type "coupon": an Apply button for a store coupon; only works for codes the store allows the assistant to offer, so never invent codes. Call it as soon as the customer has named the product and option/quantity, or asks for a discount.',
     120                'input_schema' => [
     121                    'type' => 'object',
     122                    'properties' => [
     123                        'type' => [
     124                            'type' => 'string',
     125                            'enum' => ['add', 'coupon'],
     126                            'description' => 'add = product card with selection; coupon = Apply coupon button'
     127                        ],
     128                        'product_id' => [
     129                            'type' => 'integer',
     130                            'description' => 'type add: WooCommerce product ID from a tool result'
     131                        ],
     132                        'options' => [
     133                            'type' => 'object',
     134                            'description' => 'type add, products with options: the customer\'s choice as option name → value, e.g. {"Size": "M", "Color": "Blue"}'
     135                        ],
     136                        'variation_id' => [
     137                            'type' => 'integer',
     138                            'description' => 'type add: alternatively, the variation ID from get_product_variations'
     139                        ],
     140                        'quantity' => [
     141                            'type' => 'integer',
     142                            'description' => 'type add: quantity the customer asked for (default 1)'
     143                        ],
     144                        'code' => [
     145                            'type' => 'string',
     146                            'description' => 'type coupon: coupon code'
     147                        ]
     148                    ],
     149                    'required' => ['type']
     150                ],
     151                'endpoint' => $base_url . '/propose-cart-action'
     152            ],
     153            [
    112154                'name' => 'get_order_status',
    113155                'description' => 'Look up an order. Requires BOTH an order reference AND proof that the customer owns it — an order code alone is never enough, because order numbers are sequential and guessable. Ask the customer for the email address, phone number or order key used on the order before calling this.',
    … …  
    141183            ],
    142184            [
     185                'name' => 'get_my_orders',
     186                'description' => 'List the logged-in customer\'s 5 most recent orders with number, date, status, total, items and tracking. The store account is the proof of ownership, so do not ask for an order number, email or phone. The chat shows the order you talk about as a card; mention its order number.',
     187                'input_schema' => [
     188                    'type' => 'object',
     189                    'properties' => (object) [],
     190                    'required' => []
     191                ],
     192                'endpoint' => $base_url . '/my-orders'
     193            ],
     194            [
    143195                'name' => 'get_customer_downloads',
    144196                'description' => 'Get download links for digital products in ONE order. Requires the order reference AND proof of ownership. An email address alone is not proof — it appears on invoices and in leaked lists, and these links unlock paid goods.',
    … …  
    201253                            'enum' => ['low', 'normal', 'high', 'urgent'],
    202254                            'description' => 'Mức độ ưu tiên'
     255                        ],
     256                        // Sales Insights (2.6.0, S6): missing_information = câu shop nên thêm câu trả lời.
     257                        'cause' => [
     258                            'type' => 'string',
     259                            'enum' => ['customer_asked', 'complaint', 'order_problem', 'missing_information', 'other'],
     260                            'description' => 'Why a person is needed. Use missing_information when the store data does not contain the answer.'
    203261                        ],
    204262                        'email' => [
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Admin/AdminMenu.php

    r3752682 r3765552  
    7575            'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')),
    7676            'nonce' => wp_create_nonce('wp_rest'),
    77             // Chuỗi của tab "AI Value & Performance" dịch ở PHP để vào được file .pot.
    78             'i18n' => self::value_strings(),
     77            // Chuỗi của tab "AI Value & Performance" và "Sales Insights" dịch ở PHP để vào được file .pot.
     78            'i18n' => self::value_strings() + self::insights_strings(),
    7979        ];
    8080
    … …  
    8484            $config
    8585        );
     86    }
     87
     88    /** Chuỗi giao diện của tab "Sales Insights" (2.6.0, S6); các khoá chung (period, sample, save…) ở value_strings. */
     89    private static function insights_strings() {
     90        return [
     91            'insightsTab' => __('Sales Insights', 'ninhle-ai-sales-assistant'),
     92            'insightsIntro' => __('What shoppers asked that the assistant could not answer, what they looked for but could not get, and why they did not buy. Contact details are hidden.', 'ninhle-ai-sales-assistant'),
     93            /* translators: %s: number of chats */
     94            'chatsInPeriod' => __('%s chats in this period', 'ninhle-ai-sales-assistant'),
     95            /* translators: 1: number of chats so far, 2: number of chats needed */
     96            'notEnoughYet' => __('Not enough data yet (%1$s of %2$s chats); the lists below are a first look.', 'ninhle-ai-sales-assistant'),
     97
     98            'unansweredTitle' => __('Questions the assistant could not answer', 'ninhle-ai-sales-assistant'),
     99            'unansweredHelp' => __('Similar questions are grouped. Add an answer to teach the assistant, or dismiss questions you do not want to answer. A question shows again if shoppers keep asking it after you added an answer.', 'ninhle-ai-sales-assistant'),
     100            'unansweredEmpty' => __('No unanswered questions in this period.', 'ninhle-ai-sales-assistant'),
     101            'colQuestion' => __('Question', 'ninhle-ai-sales-assistant'),
     102            'colTimes' => __('Times asked', 'ninhle-ai-sales-assistant'),
     103            'colLast' => __('Last asked', 'ninhle-ai-sales-assistant'),
     104            'colActions' => __('Actions', 'ninhle-ai-sales-assistant'),
     105            /* translators: %s: other ways the question was asked */
     106            'alsoAsked' => __('Also asked as: %s', 'ninhle-ai-sales-assistant'),
     107            'reasonCustomerNo' => __('shopper said it was not answered', 'ninhle-ai-sales-assistant'),
     108            'reasonUnknown' => __('information missing', 'ninhle-ai-sales-assistant'),
     109            'reasonNoData' => __('no matching product', 'ninhle-ai-sales-assistant'),
     110            'reasonHandoff' => __('passed to your team', 'ninhle-ai-sales-assistant'),
     111            'reasonLocalOnly' => __('local-only mode', 'ninhle-ai-sales-assistant'),
     112            'addAnswer' => __('Add answer', 'ninhle-ai-sales-assistant'),
     113            'dismiss' => __('Dismiss', 'ninhle-ai-sales-assistant'),
     114            'dismissed' => __('Question dismissed.', 'ninhle-ai-sales-assistant'),
     115            'openChat' => __('Open a chat', 'ninhle-ai-sales-assistant'),
     116            'questionLabel' => __('Question', 'ninhle-ai-sales-assistant'),
     117            'answerLabel' => __('Answer', 'ninhle-ai-sales-assistant'),
     118            'targetLegend' => __('Where to save the answer', 'ninhle-ai-sales-assistant'),
     119            'targetBusiness' => __('Add to Business information (the assistant uses it from the next reply)', 'ninhle-ai-sales-assistant'),
     120            'targetNote' => __('Save as a note so I can update my policy pages', 'ninhle-ai-sales-assistant'),
     121            'saveAnswer' => __('Save answer', 'ninhle-ai-sales-assistant'),
     122            'cancel' => __('Cancel', 'ninhle-ai-sales-assistant'),
     123            'answerSavedBusiness' => __('Answer added to Business information.', 'ninhle-ai-sales-assistant'),
     124            'answerSavedNote' => __('Answer saved as a note.', 'ninhle-ai-sales-assistant'),
     125            'answerError' => __('Could not save the answer. Enter both the question and the answer.', 'ninhle-ai-sales-assistant'),
     126            'notesTitle' => __('Notes for your policy pages', 'ninhle-ai-sales-assistant'),
     127            'notesHelp' => __('Answers you chose to add to your own pages. The assistant does not read these notes.', 'ninhle-ai-sales-assistant'),
     128            'deleteNote' => __('Delete', 'ninhle-ai-sales-assistant'),
     129
     130            'unmetTitle' => __('What shoppers could not find', 'ninhle-ai-sales-assistant'),
     131            'noResultsTitle' => __('Searches with no matching product', 'ninhle-ai-sales-assistant'),
     132            'outOfStockTitle' => __('Wanted but out of stock', 'ninhle-ai-sales-assistant'),
     133            'noneYet' => __('None in this period.', 'ninhle-ai-sales-assistant'),
     134            'searchProducts' => __('Search your products', 'ninhle-ai-sales-assistant'),
     135            'backInStock' => __('In stock now', 'ninhle-ai-sales-assistant'),
     136            'backordersOn' => __('Backorders allowed', 'ninhle-ai-sales-assistant'),
     137            'restockHint' => __('Restock it, or allow backorders under Product data > Inventory.', 'ninhle-ai-sales-assistant'),
     138
     139            'whyTitle' => __('Why shoppers did not buy', 'ninhle-ai-sales-assistant'),
     140            'whyLocked' => __('Available in the Starter plan: each night an AI reads finished chats that did not lead to an order and sorts them by reason (price, shipping, size, stock, missing information and more), with example quotes.', 'ninhle-ai-sales-assistant'),
     141            'whyOff' => __('Turn on AI labeling below to see why shoppers did not buy.', 'ninhle-ai-sales-assistant'),
     142            'whyPaused' => __('AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on.', 'ninhle-ai-sales-assistant'),
     143            /* translators: 1: chats labeled so far, 2: chats needed */
     144            'notEnoughLabels' => __('Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough.', 'ninhle-ai-sales-assistant'),
     145            'whyPrice' => __('Price', 'ninhle-ai-sales-assistant'),
     146            'whyShipping' => __('Shipping cost or time', 'ninhle-ai-sales-assistant'),
     147            'whySizeFit' => __('Unsure about size or fit', 'ninhle-ai-sales-assistant'),
     148            'whyOutOfStock' => __('Out of stock', 'ninhle-ai-sales-assistant'),
     149            'whyMissingInfo' => __('Missing information', 'ninhle-ai-sales-assistant'),
     150            'whyWaitingDiscount' => __('Waiting for a discount', 'ninhle-ai-sales-assistant'),
     151            'whyJustBrowsing' => __('Just browsing', 'ninhle-ai-sales-assistant'),
     152            'whyOther' => __('Other', 'ninhle-ai-sales-assistant'),
     153            'seeUnanswered' => __('See the unanswered questions', 'ninhle-ai-sales-assistant'),
     154            /* translators: 1: AI provider and model, 2: amount spent this month, 3: monthly limit */
     155            'labelStatus' => __('Model: %1$s · This month: %2$s of %3$s', 'ninhle-ai-sales-assistant'),
     156            /* translators: %s: date */
     157            'lastRun' => __('Last run: %s', 'ninhle-ai-sales-assistant'),
     158            'stopBudget' => __('Stopped: the monthly limit for AI labeling was reached.', 'ninhle-ai-sales-assistant'),
     159            'stopGlobalBudget' => __('Stopped: the overall AI budget was reached.', 'ninhle-ai-sales-assistant'),
     160            'stopNoProvider' => __('Stopped: no AI provider with an API key is set up.', 'ninhle-ai-sales-assistant'),
     161            'stopNightly' => __('Paused: 200 chats are labeled per night at most.', 'ninhle-ai-sales-assistant'),
     162
     163            'aiSettingsTitle' => __('Sales Insights settings', 'ninhle-ai-sales-assistant'),
     164            'aiEnable' => __('Label why shoppers did not buy, each night with AI', 'ninhle-ai-sales-assistant'),
     165            'aiEnableHelp' => __('Runs at 2:00 site time on up to 200 finished chats that did not lead to an order, using your own AI provider key. Email addresses and phone numbers are hidden before anything is sent. Off by default.', 'ninhle-ai-sales-assistant'),
     166            'aiBudget' => __('Monthly limit (USD)', 'ninhle-ai-sales-assistant'),
     167            'aiModel' => __('Model', 'ninhle-ai-sales-assistant'),
     168            'aiModelCheapest' => __('Cheapest available (recommended)', 'ninhle-ai-sales-assistant'),
     169            'aiModelChat' => __('Same as the chat assistant', 'ninhle-ai-sales-assistant'),
     170            'digestEnable' => __('Email me a weekly summary', 'ninhle-ai-sales-assistant'),
     171            'digestHelp' => __('Every Monday morning to the site admin email: top unanswered questions, what shoppers could not find, the main reason they did not buy and the AI cost of the week. Not sent in a week without chats.', 'ninhle-ai-sales-assistant'),
     172        ];
    86173    }
    87174
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Analytics/OrderAttribution.php

    r3752682 r3765552  
    2424    const META_INTERACTION_AT = '_smbcommerce_interaction_at';
    2525
     26    /** Tương tác xảy ra khi AI chạy nền, chờ lượt poll kế tiếp của khách ghi vào phiên (2.6.0). */
     27    const PENDING_TRANSIENT = 'smbcommerce_attr_pending_';
     28
    2629    public static function init() {
    2730        add_action('woocommerce_checkout_order_processed', [self::class, 'on_classic_checkout'], 20, 3);
    … …  
    3235
    3336    /**
    34      * Nhớ tương tác đủ điều kiện trong phiên WooCommerce của khách. Chỉ chạy trong request của chính
    35      * khách (widget poll, bấm link); cron và WP-CLI không có phiên của ai nên trả false.
    36      *
     37     * Nhớ tương tác đủ điều kiện trong phiên WooCommerce của khách. Chỉ ghi phiên trong request của
     38     * chính khách (widget poll, bấm link). Ở nền (cron, WP-CLI, worker AI 2.6.0) không có phiên của ai:
     39     * tương tác được giữ lại để flush_deferred() ghi ở lượt poll kế tiếp, với đúng thời điểm gốc.
     40     *
     41     * @param string|null $at Thời điểm tương tác (UTC, Y-m-d H:i:s); null = bây giờ.
    3742     * @return bool true nếu đã lưu được vào phiên.
    3843     */
    39     public static function remember_interaction($conversation_id) {
     44    public static function remember_interaction($conversation_id, $at = null) {
    4045        $conversation_id = (int) $conversation_id;
    41         if ($conversation_id <= 0 || self::is_background_context() || !function_exists('WC')) {
     46        if ($conversation_id <= 0) {
     47            return false;
     48        }
     49        $at = is_string($at) && $at !== '' ? $at : gmdate('Y-m-d H:i:s');
     50
     51        if (self::is_background_context()) {
     52            if (function_exists('set_transient')) {
     53                set_transient(self::PENDING_TRANSIENT . $conversation_id, $at, 7 * 86400);
     54            }
     55            return false;
     56        }
     57
     58        if (!function_exists('WC')) {
    4259            return false;
    4360        }
    … …  
    6683        $wc->session->set(self::SESSION_KEY, [
    6784            'conversation_id' => $conversation_id,
    68             'at' => gmdate('Y-m-d H:i:s'),
     85            'at' => $at,
    6986        ]);
    7087        return true;
     88    }
     89
     90    /**
     91     * Ghi tương tác đang chờ (từ lượt AI chạy nền) vào phiên của khách. Gọi từ request poll của chính
     92     * khách; không có gì chờ thì chỉ tốn một lần đọc transient.
     93     *
     94     * @return bool true nếu đã ghi được vào phiên.
     95     */
     96    public static function flush_deferred($conversation_id) {
     97        $conversation_id = (int) $conversation_id;
     98        if ($conversation_id <= 0 || self::is_background_context() || !function_exists('get_transient')) {
     99            return false;
     100        }
     101        $key = self::PENDING_TRANSIENT . $conversation_id;
     102        $at = get_transient($key);
     103        if (!is_string($at) || $at === '') {
     104            return false;
     105        }
     106        delete_transient($key);
     107        return self::remember_interaction($conversation_id, $at);
    71108    }
    72109
    … …  
    220257
    221258    private static function is_background_context() {
    222         return (function_exists('wp_doing_cron') && wp_doing_cron()) || (defined('WP_CLI') && WP_CLI);
     259        return \Smartbuy\SMBCommerce\Support\RequestContext::is_background();
    223260    }
    224261}
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Analytics/ValueEvents.php

    r3752682 r3765552  
    2424    const ORDER_STATUS = 'order_status';
    2525    const ORDER_REFUNDED = 'order_refunded';
     26    /** Khách thêm sản phẩm vào giỏ ngay trong chat qua Store API (2.6.0, S4); meta {product_id, qty}. */
     27    const CART_ADD = 'cart_add';
     28    /** Câu khách hỏi mà bot không trả lời được (2.6.0, S6); meta {question_norm, reason}, câu đã che SĐT/email. */
     29    const UNANSWERED = 'unanswered';
     30    /** Khách tìm mà không có hàng, hoặc chỉ có hàng hết (S6); meta {terms, result: none|out_of_stock, product_ids}. */
     31    const UNMET_DEMAND = 'unmet_demand';
     32    /** Lý do không mua do gán nhãn đêm (S6, Starter); meta {reason, quote}, trích dẫn đã che. */
     33    const NO_PURCHASE_REASON = 'no_purchase_reason';
     34    /** Khách đã biết chắc (S12): đăng nhập, hoặc đã chứng minh sở hữu đơn; meta {user_id} hoặc {order_id}. */
     35    const CUSTOMER_VERIFIED = 'customer_verified';
    2636
    2737    /** Nguồn hợp lệ của cột source; giá trị lạ được ghi là 'system'. */
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/LocalHandler.php

    r3747521 r3765552  
    187187            'email' => $params['email'] ?? '',
    188188            'phone' => $params['phone'] ?? '',
     189            'user_id' => (int) ($params['user_id'] ?? 0),
    189190        ]);
    190191
    … …  
    210211        $order = $result->data();
    211212
     213        // Thẻ đơn (S5): đơn đã qua kiểm sở hữu ở trên, ghi như kết quả tool để AIService dựng thẻ.
     214        \Smartbuy\SMBCommerce\AI\ToolResultLedger::record('get_order_status', ['success' => true, 'order' => $order]);
     215
    212216        return sprintf(
    213217            /* translators: 1: order number. 2: order status. 3: date the order was placed. */
    214218            __('Order %1$s is currently: %2$s (placed on %3$s).', 'ninhle-ai-sales-assistant'),
    215219            $order['order_number'],
    216             $order['status'],
    217             $order['date']
     220            function_exists('wc_get_order_status_name') ? wc_get_order_status_name($order['status']) : $order['status'],
     221            // Ngày theo định dạng của site; present() trả giờ địa phương của shop dạng Y-m-d H:i:s.
     222            function_exists('date_i18n') && strtotime($order['date'])
     223                ? date_i18n((string) get_option('date_format', 'Y-m-d'), strtotime($order['date']))
     224                : $order['date']
    218225        );
    219226    }
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/LocalResolver.php

    r3747521 r3765552  
    7575
    7676    /**
     77     * Lệnh giỏ hàng/thanh toán là câu ngắn. Câu dài hơn thường hỏi về sản phẩm có chữ "thanh toán"/"cart"
     78     * ("Shop có plugin nào nhận thanh toán bằng mã QR cho WooCommerce không?", ninhle.com 25/10): để AI trả lời.
     79     */
     80    const MAX_COMMAND_LENGTH = 60;
     81
     82    /** Dấu hiệu câu hỏi: "thanh toán" trong câu hỏi là hỏi về cách trả tiền hay sản phẩm, không phải lệnh. */
     83    const QUESTION_MARKERS = [
     84        'không', 'nào', 'gì', 'bao nhiêu', 'thế nào', 'như nào', 'làm sao', 'được chưa', 'hỗ trợ',
     85        'how', 'what', 'which', 'do you', 'does', 'can i', 'is there', 'are there', 'accept',
     86    ];
     87
     88    /**
    7789     * Quyết định định tuyến cho một tin nhắn.
    7890     *
    … …  
    124136        }
    125137
    126         // --- Giỏ hàng và thanh toán ---
    127         if (self::contains_any($text, self::CHECKOUT_KEYWORDS)) {
     138        // --- Giỏ hàng và thanh toán: chỉ câu lệnh ngắn. "Thanh toán" trong câu hỏi thì để AI. ---
     139        $short = self::length($raw) <= self::MAX_COMMAND_LENGTH;
     140        $question = strpos($raw, '?') !== false || self::has_word_from($text, self::QUESTION_MARKERS);
     141        if ($short && !$question && self::contains_any($text, self::CHECKOUT_KEYWORDS)) {
    128142            return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'checkout');
    129143        }
    130144
    131         if (self::contains_any($text, self::CART_KEYWORDS)) {
     145        // Câu hỏi về giỏ của chính khách ("giỏ hàng của tôi có gì?") vẫn trả lời tại chỗ: AI chạy nền không đọc được giỏ.
     146        if ($short && self::contains_any($text, self::CART_KEYWORDS)) {
    132147            return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'view_cart');
    133148        }
    … …  
    178193    private static function length($text) {
    179194        return function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
     195    }
     196
     197    /** Câu (đã normalize) có chứa một trong các cụm, xét theo ranh giới từ ("nào" không khớp "nàoo"). */
     198    private static function has_word_from($text, array $phrases) {
     199        $padded = ' ' . $text . ' ';
     200        foreach ($phrases as $phrase) {
     201            if (strpos($padded, ' ' . self::normalize($phrase) . ' ') !== false) {
     202                return true;
     203            }
     204        }
     205        return false;
    180206    }
    181207
    … …  
    253279        }
    254280
     281        // Khách đã đăng nhập (S5): tài khoản là bằng chứng, OrderService chỉ khớp đơn của chính tài khoản đó.
     282        $customer = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id();
     283        if ($customer > 0) {
     284            $proof['user_id'] = $customer;
     285        }
     286
    255287        if (empty($proof)) {
    256288            return null;
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/CartService.php

    r3747521 r3765552  
    1818 * Vì sao điều đó nguy hiểm ở đây:
    1919 *
    20  *   Lời gọi tool của AI chạy trong MỘT TRONG HAI ngữ cảnh, và plugin không chọn
    21  *   được ngữ cảnh nào:
    22  *
    23  *     1. Request poll của widget (ChatController::get_messages -> run_one).
    24  *        Đây là request của chính khách, mang cookie của họ, nên WC()->cart
    25  *        đúng là giỏ của người đang chat.
    26  *
    27  *     2. Cron mỗi phút (JobDispatcher::CRON_HOOK), lo nốt hội thoại của khách
    28  *        đã đóng tab. Ở đây KHÔNG có cookie của ai cả.
    29  *
    30  *   Cùng một câu "thêm vào giỏ giúp em" sẽ rơi vào ngữ cảnh nào là tuỳ lúc
    31  *   khách còn mở tab hay không. Nếu cứ ghi bừa, kết quả là: lúc được lúc không,
    32  *   và tệ nhất là trên hosting có cache/session lạ thì ghi vào giỏ của NGƯỜI
    33  *   KHÁC — người vô tình kích hoạt cron.
     20 *   Lời gọi tool của AI chạy trong MỘT TRONG HAI loại ngữ cảnh, và plugin không
     21 *   chọn được ngữ cảnh nào:
     22 *
     23 *     1. Request poll của widget (ChatController::get_messages), với câu về giỏ
     24 *        hàng hoặc khi chạy nền bị chặn. Đây là request của chính khách, mang
     25 *        cookie của họ, nên WC()->cart đúng là giỏ của người đang chat.
     26 *
     27 *     2. Nền (2.6.0, AsyncKick): sau khi đã trả phản hồi, request loopback, hoặc
     28 *        cron mỗi phút cho khách đã đóng tab. Ở đây KHÔNG có cookie của ai cả,
     29 *        hoặc có nhưng không đặt được cookie mới.
     30 *
     31 *   Cùng một câu "thêm vào giỏ giúp em" sẽ rơi vào ngữ cảnh nào là tuỳ hosting
     32 *   và lúc khách còn mở tab hay không. Nếu cứ ghi bừa, kết quả là: lúc được lúc
     33 *   không, và tệ nhất là trên hosting có cache/session lạ thì ghi vào giỏ của
     34 *   NGƯỜI KHÁC — người vô tình kích hoạt cron.
    3435 *
    3536 * Nguyên tắc: chỉ động vào giỏ khi chắc chắn đang ở trong request của chính
    … …  
    252253
    253254    /**
    254      * Đang chạy ngoài request của khách hay không.
    255      *
    256      * Mỗi điều kiện ở đây tương ứng một đường thật sự tồn tại trong plugin:
    257      * cron chạy job tồn đọng, WP-CLI khi quản trị viên chạy lệnh, và giai đoạn
    258      * cài đặt/nâng cấp.
     255     * Đang chạy ngoài request của khách hay không: cron, WP-CLI, cài đặt/nâng cấp, hoặc worker nền
     256     * của plugin. Giữ tên cũ cho code gọi sẵn; định nghĩa nằm ở Support\RequestContext.
    259257     */
    260258    public static function is_background_context() {
    261         if (defined('DOING_CRON') && DOING_CRON) {
    262             return true;
    263         }
    264 
    265         if (function_exists('wp_doing_cron') && wp_doing_cron()) {
    266             return true;
    267         }
    268 
    269         if (defined('WP_CLI') && WP_CLI) {
    270             return true;
    271         }
    272 
    273         if (defined('WP_INSTALLING') && WP_INSTALLING) {
    274             return true;
    275         }
    276 
    277         return false;
     259        return \Smartbuy\SMBCommerce\Support\RequestContext::is_background();
    278260    }
    279261
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/OrderService.php

    r3747521 r3765552  
    3333    const PUBLIC_FIELDS = [
    3434        'id', 'order_number', 'status', 'date', 'total', 'currency', 'items',
    35         'payment_method', 'shipping_method',
     35        'payment_method', 'shipping_method', 'tracking',
    3636    ];
    3737
    … …  
    134134
    135135        return CommerceResult::success(['downloads' => $downloads]);
     136    }
     137
     138    /**
     139     * Đơn gần nhất của khách đã đăng nhập (2.6.0, S5, tool get_my_orders).
     140     *
     141     * Không cần mã đơn hay email: tài khoản WordPress là bằng chứng. $user_id PHẢI là khách của lượt
     142     * (Support\CustomerContext), không bao giờ lấy từ tham số của model.
     143     */
     144    public function recent_for_customer($user_id, $limit = 5) {
     145        if (!self::woocommerce_active()) {
     146            return CommerceResult::woocommerce_missing();
     147        }
     148        $user_id = (int) $user_id;
     149        if ($user_id <= 0) {
     150            return CommerceResult::forbidden('The customer is not logged in. Ask for the order number and the email or phone number used on the order, then call get_order_status.');
     151        }
     152
     153        $orders = wc_get_orders([
     154            'customer_id' => $user_id,
     155            'type' => 'shop_order',
     156            'status' => function_exists('wc_get_order_statuses') ? array_keys(wc_get_order_statuses()) : 'any',
     157            'limit' => max(1, min(10, (int) $limit)),
     158            'orderby' => 'date',
     159            'order' => 'DESC',
     160        ]);
     161
     162        $out = [];
     163        foreach ((array) $orders as $order) {
     164            // Lớp bảo vệ thứ hai: chỉ đơn đúng của tài khoản này, kể cả khi bộ lọc của truy vấn bị plugin khác đổi.
     165            if (is_object($order) && (int) $order->get_customer_id() === $user_id) {
     166                $out[] = self::present($order);
     167            }
     168        }
     169
     170        return CommerceResult::success(['orders' => $out, 'count' => count($out)]);
    136171    }
    137172
    … …  
    278313            'payment_method' => $order->get_payment_method_title(),
    279314            'shipping_method' => $order->get_shipping_method(),
     315            // Mã vận đơn (S5): thẻ đơn hiện mã mà model không biết thì nó nói "chưa gửi hàng" (staging 25/10).
     316            'tracking' => array_map(function ($t) {
     317                return ['code' => $t['code'], 'carrier' => $t['carrier'], 'shipped' => $t['date']];
     318            }, \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::for_order($order)),
    280319        ];
    281320    }
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/ProductImage.php

    r3761175 r3765552  
    2121    const SIZES = ['large', 'medium_large', 'medium', 'full'];
    2222
     23    /** Thẻ sản phẩm trong chat (2.6.0) hiện ảnh 64 px: bản nhỏ trước, đủ nét trên màn hình 2x. */
     24    const CARD_SIZES = ['thumbnail', 'medium', 'medium_large', 'large', 'full'];
     25
    2326    /**
    24      * @param int $attachment_id ID ảnh đại diện của sản phẩm.
     27     * @param int      $attachment_id ID ảnh đại diện của sản phẩm.
     28     * @param string[] $sizes         Thứ tự cỡ ảnh thử lần lượt.
    2529     * @return string URL dùng được, hoặc chuỗi rỗng khi không có ảnh nào còn trên đĩa.
    2630     */
    27     public static function url($attachment_id) {
     31    public static function url($attachment_id, array $sizes = self::SIZES) {
    2832        $attachment_id = (int) $attachment_id;
    2933        if ($attachment_id <= 0 || !function_exists('wp_get_attachment_image_src')) {
    … …  
    3438        $tried = [];
    3539
    36         foreach (self::SIZES as $size) {
     40        foreach ($sizes as $size) {
    3741            $src = wp_get_attachment_image_src($attachment_id, $size);
    3842            $url = is_array($src) && !empty($src[0]) ? (string) $src[0] : '';
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/ProductService.php

    r3761175 r3765552  
    5757        // Danh mục rỗng khác hẳn với "không tìm thấy". Nói rõ để model khuyên
    5858        // chủ shop đồng bộ, thay vì trả lời khách là cửa hàng không có gì bán.
    59         if (empty($products) && $scanner->count_cached() === 0) {
    60             $meta['warning'] = 'The product catalog has not been synced yet. Go to Ninhle AI Sales Assistant > Settings and run a product sync.';
     59        $data = [
     60            'count' => count($products),
     61            'currency' => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : '',
     62            'products' => $products,
     63        ];
     64        // Starter (S4): nhắc ngay trong kết quả tool, chỗ model đọc trước bước kế tiếp. Chỉ có câu này trong system
     65        // prompt thì AI thật lúc đặt sẵn, lúc lại bảo khách tự chọn trên thẻ (staging 25/10).
     66        if (!empty($products) && class_exists('\\Smartbuy\\SMBCommerce\\Commerce\\Woo\\CartActionService')
     67            && CartActionService::enabled()) {
     68            $meta['guidance'] = 'If the customer has already said which of these products they want and the option (such as size or colour) or quantity, '
     69                . 'do not ask more questions: call propose_cart_action now with type "add", the product_id, their choice in options '
     70                . '(for example {"Size": "M"}) and the quantity.';
     71        }
     72
     73        if (empty($products)) {
     74            if ($scanner->count_cached() === 0) {
     75                $meta['warning'] = 'The product catalog has not been synced yet. Go to Ninhle AI Sales Assistant > Settings and run a product sync.';
     76            } elseif ($keyword !== '') {
     77                // Cứu lượt tìm hụt (2.6.0, S3): gợi ý gần nhất để model hỏi lại một câu, không bịa sản phẩm.
     78                $rescue = self::rescue($keyword, $scanner);
     79                if ($rescue) {
     80                    $data['suggestions'] = $rescue;
     81                }
     82            }
    6183        }
    6284
    6385        // Giá trong danh sách là số trơn: kèm mã tiền tệ để model không tự đoán "$".
    64         return CommerceResult::success(
    65             [
    66                 'count' => count($products),
    67                 'currency' => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : '',
    68                 'products' => $products,
    69             ],
    70             $meta
    71         );
     86        return CommerceResult::success($data, $meta);
     87    }
     88
     89    /** Điểm tối thiểu (0–100) để một danh mục/tag được coi là gần với từ khoá. */
     90    const RESCUE_MIN_SCORE = 45;
     91
     92    /**
     93     * Danh mục và tag sản phẩm gần nhất với từ khoá tìm hụt, cùng tối đa 3 sản phẩm bán chạy còn hàng của
     94     * danh mục đầu. Điểm = độ giống chuỗi (similar_text) + 25 cho mỗi từ chung có từ 3 chữ cái.
     95     *
     96     * @return array|null ['categories' => [...], 'products' => [...], 'note' => ...] hoặc null.
     97     */
     98    public static function rescue($keyword, WooCommerceProductScanner $scanner) {
     99        if (!function_exists('get_terms')) {
     100            return null;
     101        }
     102        $needle = self::fold($keyword);
     103        $needle_words = array_filter(preg_split('/[^\p{L}\p{N}]+/u', $needle, -1, PREG_SPLIT_NO_EMPTY), function ($w) {
     104            return mb_strlen($w, 'UTF-8') >= 3;
     105        });
     106
     107        $scored = [];
     108        foreach (['product_cat', 'product_tag'] as $taxonomy) {
     109            $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => true, 'number' => 300]);
     110            if (!is_array($terms)) {
     111                continue;
     112            }
     113            foreach ($terms as $term) {
     114                if (!is_object($term) || ($term->slug ?? '') === 'uncategorized') {
     115                    continue;
     116                }
     117                $name = html_entity_decode((string) $term->name, ENT_QUOTES, 'UTF-8');
     118                $hay = self::fold($name);
     119                similar_text($needle, $hay, $percent);
     120                $hay_words = preg_split('/[^\p{L}\p{N}]+/u', $hay, -1, PREG_SPLIT_NO_EMPTY);
     121                $shared = 0;
     122                foreach ($needle_words as $word) {
     123                    foreach ($hay_words as $candidate) {
     124                        // "jacket" khớp "jackets", "áo khoác" khớp "khoác": tiền tố chung từ 3 chữ cái.
     125                        if (strpos($candidate, $word) === 0 || (strpos($word, $candidate) === 0 && mb_strlen($candidate, 'UTF-8') >= 3)) {
     126                            $shared++;
     127                            break;
     128                        }
     129                    }
     130                }
     131                $score = $percent + 25 * $shared;
     132                if ($score >= self::RESCUE_MIN_SCORE) {
     133                    $key = $taxonomy . ':' . $name;
     134                    $scored[$key] = ['name' => $name, 'type' => $taxonomy === 'product_cat' ? 'category' : 'tag', 'score' => $score];
     135                }
     136            }
     137        }
     138        if (empty($scored)) {
     139            return null;
     140        }
     141        uasort($scored, function ($a, $b) {
     142            return $b['score'] <=> $a['score'];
     143        });
     144        $top = array_slice(array_values($scored), 0, 3);
     145
     146        $products = [];
     147        foreach ($top as $match) {
     148            if ($match['type'] === 'category') {
     149                $products = $scanner->search_products('', $match['name'], 0, 0, 3, true);
     150                break;
     151            }
     152        }
     153
     154        return [
     155            'categories' => array_map(function ($m) {
     156                return ['name' => $m['name'], 'type' => $m['type']];
     157            }, $top),
     158            'products' => is_array($products) ? $products : [],
     159            'note' => 'No product matched the search exactly. Ask the customer one short clarifying question, and you may suggest these categories or show these products with [[show:ID]]. Never present them as an exact match.',
     160        ];
     161    }
     162
     163    /** Chữ thường, gộp khoảng trắng; giữ dấu tiếng Việt như LocalResolver. */
     164    private static function fold($text) {
     165        return trim((string) preg_replace('/\s+/u', ' ', mb_strtolower((string) $text, 'UTF-8')));
    72166    }
    73167
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Conversation/ConversationRepository.php

    r3752682 r3765552  
    3030    }
    3131   
     32    /** Ngôn ngữ khách đang viết (2.6.0, ReplyLanguage::observe). */
     33    public static function set_language($conversation_id, $language) {
     34        global $wpdb;
     35        return $wpdb->update(
     36            $wpdb->prefix . SMBCOMMERCE_PREFIX . 'conversations',
     37            ['language' => substr(sanitize_key((string) $language), 0, 16)],
     38            ['id' => (int) $conversation_id],
     39            ['%s'],
     40            ['%d']
     41        );
     42    }
     43
    3244    public static function create_conversation($contact_id) {
    3345        global $wpdb;
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Deactivator.php

    r3752682 r3765552  
    1313        wp_clear_scheduled_hook('smbcommerce_run_jobs');
    1414        wp_clear_scheduled_hook('smbcommerce_finalize_outcomes');
     15        // Sales Insights (2.6.0): gán nhãn đêm và bản tin tuần, kể cả đợt nối tiếp.
     16        \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule();
    1517
    1618        flush_rewrite_rules();
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/JobDispatcher.php

    r3752682 r3765552  
    99 * Nối hàng đợi job với phần xử lý thật.
    1010 *
    11  * Không dùng request loopback: nhiều hosting chặn, staging hay có basic-auth,
    12  * và nó bắt buộc phải mở thêm một endpoint công khai. Thay vào đó, chính
    13  * request poll của widget (5 giây/lần) đóng vai worker — luôn chạy được, và
    14  * cron mỗi phút lo nốt hội thoại mà khách đã đóng tab.
     11 * Từ 2.6.0 job AI được AsyncKick đá chạy nền ngay khi khách gửi tin. Request poll
     12 * của widget chỉ còn ĐỌC, trừ ba trường hợp nó tự chạy job của CHÍNH hội thoại
     13 * đó (không bao giờ job của khách khác như run_one() trước đây):
     14 *   - job 'visitor' (câu về giỏ hàng): cần cookie phiên của khách, chạy ngay;
     15 *   - chế độ legacy (chạy nền bị host chặn): chạy ngay, như 2.5.0;
     16 *   - job đã chờ quá GRACE_SECONDS mà nền chưa nhận: chạy bù và ghi nhận.
     17 * Cron mỗi phút lo nốt hội thoại mà khách đã đóng tab.
    1518 */
    1619class JobDispatcher {
    … …  
    1821    const CRON_HOOK = 'smbcommerce_run_jobs';
    1922    const SCHEDULE = 'smbcommerce_every_minute';
     23
     24    /** Poll chạy bù job đã được đá nhưng chưa ai nhận sau ngần này giây. */
     25    const GRACE_SECONDS = 4;
     26
     27    /**
     28     * Khi nền còn sống mà hàng đợi đầy: lưới an toàn, poll chỉ chạy bù sau ngần này giây. 60 (không phải 20):
     29     * thử tải 2 job trên hosting dùng chung cho thấy hàng đợi thường dài quá 20 s, và poll giành job đúng lúc
     30     * quá tải chỉ chuyển việc sang worker của khách (poll p95 6 s). Worker nền làm tiếp và cron mỗi phút vẫn chạy.
     31     */
     32    const CONGESTION_GRACE_SECONDS = 60;
    2033
    2134    public static function init() {
    … …  
    5265        }
    5366
     67        // Chạy trong locale của khách (S2): câu cố định, câu trả lời tại chỗ và chế độ local-only ra đúng
     68        // ngôn ngữ khách khi site có bản dịch.
     69        // Khách đã đăng nhập lúc gửi tin (S5): job chạy nền không có người dùng của khách, và request đang
     70        // chạy job có thể là của người khác (admin lúc WP-Cron chạy). Luôn đặt từ payload, kể cả 0.
     71        \Smartbuy\SMBCommerce\Support\CustomerContext::run((int) ($payload['wp_user_id'] ?? 0), function () use ($conversation_id, $payload) {
     72            \Smartbuy\SMBCommerce\AI\ReplyLanguage::in_conversation_language($conversation_id, function () use ($conversation_id, $payload) {
     73                self::process($conversation_id, $payload);
     74            });
     75        });
     76    }
     77
     78    private static function process($conversation_id, $payload) {
     79        // Lượt chạy nền có ngân sách thời gian riêng (TurnClock): chờ nhà cung cấp lâu hơn, vẫn xong trước khi
     80        // khoá job hết hạn.
     81        if (\Smartbuy\SMBCommerce\Support\RequestContext::is_background()) {
     82            \Smartbuy\SMBCommerce\AI\TurnClock::start_background_turn();
     83        }
    5484        try {
    5585            \Smartbuy\SMBCommerce\AI\AIService::process_user_message(
    … …  
    70100                );
    71101            }
     102        } finally {
     103            \Smartbuy\SMBCommerce\AI\TurnClock::clear();
    72104        }
    73105    }
    … …  
    81113
    82114    /**
    83      * Được gọi từ request poll của widget: chỉ làm một job để request không bị
    84      * kéo dài quá lâu, rồi trả tin nhắn về ngay.
     115     * Lượt poll của một hội thoại: một truy vấn đọc job còn dở; chỉ chạy job khi rơi vào ba trường
     116     * hợp ở đầu lớp.
     117     *
     118     * @return array ['pending' => số job còn dở, 'ran' => đã chạy job không, 'queued' => đang chờ slot]
    85119     */
    86     public static function run_one() {
    87         return JobQueue::run_due(1, JobQueue::ACTION_PROCESS_MESSAGE);
     120    public static function poll_turn($conversation_id) {
     121        $jobs = JobQueue::for_conversation($conversation_id);
     122        $turn = ['pending' => count($jobs), 'ran' => false, 'queued' => false];
     123
     124        if (empty($jobs)) {
     125            return $turn;
     126        }
     127
     128        foreach ($jobs as $job) {
     129            if ($job['status'] === 'running') {
     130                return $turn;
     131            }
     132        }
     133
     134        $job = $jobs[0];
     135        $now = current_time('mysql');
     136        if (strtotime((string) $job['next_attempt_at']) > strtotime($now)) {
     137            // Đang chờ thử lại sau lỗi: không ép chạy sớm.
     138            return $turn;
     139        }
     140
     141        $payload = $job['payload'];
     142        $visitor = !empty($payload['visitor']);
     143        $kicked = !empty($payload['kick']) && $payload['kick'] !== AsyncKick::STRATEGY_LEGACY;
     144
     145        // Nền còn sống: job chờ là do hàng đợi đầy, để worker nền làm (chỉ chạy bù sau CONGESTION_GRACE).
     146        // Nền im lặng: chạy bù sau GRACE và ghi "bỏ lỡ".
     147        $alive = $kicked && !$visitor && AsyncKick::background_alive();
     148        if (!$visitor && $kicked && !AsyncKick::is_legacy()) {
     149            $grace = $alive ? self::CONGESTION_GRACE_SECONDS : self::GRACE_SECONDS;
     150            if (JobQueue::age_seconds($job['created_at'], $now) < $grace) {
     151                $turn['queued'] = $alive && JobQueue::is_saturated();
     152                return $turn;
     153            }
     154        }
     155
     156        $claimed = JobQueue::claim_by_id($job['id']);
     157        if (!$claimed) {
     158            $turn['queued'] = JobQueue::is_saturated();
     159            return $turn;
     160        }
     161
     162        if ($kicked && !$visitor && !$alive) {
     163            AsyncKick::note_missed();
     164        }
     165        JobQueue::execute($claimed);
     166        $turn['ran'] = true;
     167        $turn['pending']--;
     168
     169        return $turn;
    88170    }
    89171}
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/JobQueue.php

    r3747521 r3765552  
    1212 * (có thể mất hàng chục giây khi phải gọi tool nhiều vòng) chạy tách ra.
    1313 *
    14  * Job được rút ra bởi, theo thứ tự nhanh dần tới chậm:
    15  *   1. Request loopback không chờ, bắn ngay sau khi enqueue (nhanh nhất)
    16  *   2. Chính request poll của widget (5 giây/lần) — luôn hoạt động, không phụ
    17  *      thuộc loopback hay cron, nên đây là lớp bảo đảm cuối
    18  *   3. Cron mỗi phút, cho hội thoại khách đã đóng tab
     14 * Job được rút ra bởi (2.6.0, xem AsyncKick và JobDispatcher):
     15 *   1. Worker nền được "đá" ngay sau khi enqueue: chạy tiếp sau khi đã trả phản
     16 *      hồi cho khách, hoặc qua request loopback. Xong job thì làm tiếp job chờ.
     17 *   2. Request poll của CHÍNH hội thoại đó, khi job đã chờ quá thời gian ân hạn
     18 *      (chạy nền bị host chặn) hoặc job cần phiên của khách (câu về giỏ hàng).
     19 *   3. Cron mỗi phút, lưới an toàn cho mọi thứ còn sót.
     20 *
     21 * Mỗi hội thoại chỉ có một job chạy tại một thời điểm: hai câu gửi liền nhau
     22 * không được sinh hai câu trả lời song song.
    1923 */
    2024class JobQueue {
    … …  
    2529    const MAX_ATTEMPTS = 3;
    2630
    27     /** Giới hạn job AI chạy đồng thời trên toàn site, chống cạn kiệt PHP worker. */
    28     const MAX_CONCURRENT = 4;
     31    /**
     32     * Giới hạn mặc định số job AI chạy đồng thời trên toàn site, chống cạn kiệt PHP worker. 2 (không phải 3)
     33     * sau thử tải trên hosting dùng chung 24/10/2026; shop có máy chủ riêng tăng bằng option dưới.
     34     */
     35    const DEFAULT_CONCURRENT = 2;
     36
     37    /** Trần cứng của giới hạn đồng thời, kể cả khi option hay filter đặt cao hơn. */
     38    const MAX_CONCURRENT = 8;
     39
     40    /** Option (không có giao diện, đặt bằng WP-CLI) để chỉnh giới hạn đồng thời. */
     41    const CONCURRENCY_OPTION = 'smbcommerce_ai_max_concurrency';
    2942
    3043    /** Job bị coi là kẹt (worker chết giữa chừng) sau ngần này giây. */
    3144    const STALE_LOCK_SECONDS = 180;
    3245
     46    /**
     47     * Job đánh dấu 'visitor' (câu về giỏ hàng, cần cookie phiên của khách) được nhường cho poll của
     48     * chính khách trong ngần này giây; quá hạn (khách đã đóng tab) thì worker nền và cron mới nhận.
     49     */
     50    const VISITOR_WAIT_SECONDS = 30;
     51
    3352    private static function table() {
    3453        global $wpdb;
    … …  
    3655    }
    3756
     57    /**
     58     * conversation_id luôn là khoá ĐẦU TIÊN của payload: for_conversation() tìm job của một hội thoại
     59     * bằng tiền tố JSON đó mà không cần thêm cột.
     60     *
     61     * @return int ID job mới (0 nếu ghi thất bại).
     62     */
    3863    public static function enqueue($action, $payload = []) {
    3964        global $wpdb;
    4065
     66        $payload = self::conversation_first((array) $payload);
    4167        $now = current_time('mysql');
    4268
    43         $wpdb->insert(self::table(), [
     69        $inserted = $wpdb->insert(self::table(), [
    4470            'action' => $action,
    4571            'payload' => wp_json_encode($payload),
    … …  
    5076        ]);
    5177
    52         return $wpdb->insert_id;
    53     }
    54 
    55     /**
    56      * Giành lấy một job đang chờ. Dùng UPDATE có điều kiện nên hai worker chạy
    57      * song song không thể cùng nhận một job.
     78        // Ghi lỗi thì insert_id vẫn giữ ID của lần ghi trước: không được đá nhầm job của người khác.
     79        return $inserted ? (int) $wpdb->insert_id : 0;
     80    }
     81
     82    /** Giới hạn đồng thời đang áp dụng: option, rồi filter, kẹp trong 1..MAX_CONCURRENT. */
     83    public static function max_concurrent() {
     84        $limit = (int) get_option(self::CONCURRENCY_OPTION, self::DEFAULT_CONCURRENT);
     85        $limit = (int) apply_filters('smbcommerce_ai_max_concurrency', $limit > 0 ? $limit : self::DEFAULT_CONCURRENT);
     86        return max(1, min(self::MAX_CONCURRENT, $limit));
     87    }
     88
     89    /** Đã hết slot chạy đồng thời chưa (job mới sẽ phải xếp hàng). */
     90    public static function is_saturated() {
     91        return self::running_count() >= self::max_concurrent();
     92    }
     93
     94    /**
     95     * Giành lấy job đang chờ lâu nhất. Dùng UPDATE có điều kiện nên hai worker chạy
     96     * song song không thể cùng nhận một job. Bỏ qua job của hội thoại đang có job chạy,
     97     * và job 'visitor' còn trong thời gian nhường cho poll của khách.
    5898     *
    5999     * @return array|null Job đã giành được, hoặc null nếu không có gì để làm.
    … …  
    61101    public static function claim($filter_action = null) {
    62102        global $wpdb;
    63         $table = self::table();
    64103
    65104        self::release_stale_locks();
    66105
    67         if (self::running_count() >= self::MAX_CONCURRENT) {
    68             return null;
    69         }
    70 
    71         $worker_id = wp_generate_uuid4();
    72         $now = current_time('mysql');
     106        if (self::is_saturated()) {
     107            return null;
     108        }
    73109
    74110        // Lọc theo action là tuỳ chọn: chuỗi rỗng nghĩa là mọi action (câu SQL cố định).
    75111        $action = $filter_action !== null ? (string) $filter_action : '';
    76 
    77         $candidate = $wpdb->get_row($wpdb->prepare(
    78             "SELECT id FROM %i
     112        $now = current_time('mysql');
     113
     114        $candidates = $wpdb->get_results($wpdb->prepare(
     115            "SELECT id, payload, created_at FROM %i
    79116             WHERE status = 'pending' AND next_attempt_at <= %s AND (%s = '' OR action = %s)
    80              ORDER BY id ASC LIMIT 1",
    81             $table,
     117             ORDER BY id ASC LIMIT 10",
     118            self::table(),
    82119            $now,
    83120            $action,
    … …  
    85122        ), ARRAY_A);
    86123
    87         if (!$candidate) {
    88             return null;
    89         }
    90 
    91         // Chỉ giành được nếu job vẫn còn 'pending' ở thời điểm UPDATE chạy.
    92         $claimed = $wpdb->query($wpdb->prepare(
    93             "UPDATE %i SET status = 'running', locked_by = %s, updated_at = %s
    94              WHERE id = %d AND status = 'pending'",
    95             $table,
    96             $worker_id,
    97             $now,
    98             $candidate['id']
    99         ));
    100 
    101         if (!$claimed) {
    102             return null;
    103         }
    104 
    105         $job = $wpdb->get_row($wpdb->prepare(
    106             'SELECT * FROM %i WHERE id = %d',
    107             $table,
    108             $candidate['id']
     124        if (empty($candidates)) {
     125            return null;
     126        }
     127
     128        $busy = self::running_conversations();
     129        foreach ($candidates as $candidate) {
     130            $payload = json_decode((string) $candidate['payload'], true) ?: [];
     131            $conversation_id = (int) ($payload['conversation_id'] ?? 0);
     132            if ($conversation_id > 0 && isset($busy[$conversation_id])) {
     133                continue;
     134            }
     135            if (!empty($payload['visitor']) && self::age_seconds($candidate['created_at'], $now) < self::VISITOR_WAIT_SECONDS) {
     136                continue;
     137            }
     138            $job = self::lock((int) $candidate['id']);
     139            if ($job) {
     140                return $job;
     141            }
     142        }
     143
     144        return null;
     145    }
     146
     147    /** Số giây từ $created_at tới $now (cả hai theo giờ site, dạng MySQL). */
     148    public static function age_seconds($created_at, $now = null) {
     149        $now = $now ?: current_time('mysql');
     150        return max(0, strtotime((string) $now) - strtotime((string) $created_at));
     151    }
     152
     153    /**
     154     * Giành đúng một job (worker nền được đá cho job vừa tạo). Cùng điều kiện như claim():
     155     * còn slot, job đang chờ và đã tới hạn, hội thoại không có job khác đang chạy.
     156     *
     157     * @return array|null
     158     */
     159    public static function claim_by_id($job_id) {
     160        global $wpdb;
     161
     162        $job_id = (int) $job_id;
     163        if ($job_id <= 0 || self::is_saturated()) {
     164            return null;
     165        }
     166
     167        $row = $wpdb->get_row($wpdb->prepare(
     168            "SELECT id, payload FROM %i WHERE id = %d AND status = 'pending' AND next_attempt_at <= %s",
     169            self::table(),
     170            $job_id,
     171            current_time('mysql')
    109172        ), ARRAY_A);
    110173
    111         if ($job) {
    112             $job['payload'] = json_decode($job['payload'], true) ?: [];
    113         }
    114 
    115         return $job;
     174        if (!$row) {
     175            return null;
     176        }
     177
     178        $conversation_id = self::payload_conversation($row['payload']);
     179        if ($conversation_id > 0 && isset(self::running_conversations()[$conversation_id])) {
     180            return null;
     181        }
     182
     183        return self::lock($job_id);
     184    }
     185
     186    /**
     187     * Job chưa xong (đang chờ hoặc đang chạy) của một hội thoại, cũ nhất trước. Một truy vấn,
     188     * bảng jobs luôn nhỏ vì job xong là bị xoá.
     189     *
     190     * @return array[] Mỗi phần tử: id, action, status, payload (đã giải mã), next_attempt_at, created_at.
     191     */
     192    public static function for_conversation($conversation_id) {
     193        global $wpdb;
     194
     195        $conversation_id = (int) $conversation_id;
     196        if ($conversation_id <= 0) {
     197            return [];
     198        }
     199
     200        // Tiền tố JSON cố định do enqueue() ghi; dấu phẩy hoặc ngoặc đóng ngay sau số nên hội thoại
     201        // 12 không khớp nhầm 123. Dấu '_' trong LIKE khớp mọi ký tự, kể cả chính nó, nên không cần
     202        // esc_like; kết quả vẫn được lọc lại sau khi giải mã.
     203        $rows = $wpdb->get_results($wpdb->prepare(
     204            "SELECT id, action, status, payload, next_attempt_at, created_at FROM %i
     205             WHERE status IN ('pending', 'running') AND (payload LIKE %s OR payload = %s)
     206             ORDER BY id ASC LIMIT 5",
     207            self::table(),
     208            '{"conversation_id":' . $conversation_id . ',%',
     209            '{"conversation_id":' . $conversation_id . '}'
     210        ), ARRAY_A);
     211
     212        $jobs = [];
     213        foreach ((array) $rows as $row) {
     214            $row['payload'] = json_decode((string) $row['payload'], true) ?: [];
     215            if ((int) ($row['payload']['conversation_id'] ?? 0) === $conversation_id) {
     216                $jobs[] = $row;
     217            }
     218        }
     219        return $jobs;
    116220    }
    117221
    … …  
    151255                'status' => 'pending',
    152256                'locked_by' => null,
    153                 'payload' => wp_json_encode($payload),
     257                'payload' => wp_json_encode(self::conversation_first($payload)),
    154258                'next_attempt_at' => gmdate('Y-m-d H:i:s', strtotime(current_time('mysql')) + $delay),
    155259                'updated_at' => current_time('mysql'),
    … …  
    161265    }
    162266
     267    /** Chạy một job đã giành được: xong thì xoá, lỗi thì hẹn thử lại. */
     268    public static function execute(array $job) {
     269        try {
     270            do_action('smbcommerce_run_job_' . $job['action'], $job['payload'], $job);
     271            self::complete($job['id']);
     272            return true;
     273        } catch (\Throwable $e) {
     274            self::fail($job, $e->getMessage());
     275            return false;
     276        }
     277    }
     278
    163279    /**
    164280     * Rút và chạy tối đa $limit job. Trả về số job đã xử lý.
    … …  
    173289            }
    174290
    175             try {
    176                 do_action('smbcommerce_run_job_' . $job['action'], $job['payload'], $job);
    177                 self::complete($job['id']);
    178             } catch (\Throwable $e) {
    179                 self::fail($job, $e->getMessage());
    180             }
    181 
     291            self::execute($job);
    182292            $processed++;
    183293        }
    … …  
    186296    }
    187297
     298    /** Đổi job sang 'running' nếu nó vẫn còn 'pending' ở thời điểm UPDATE chạy. */
     299    private static function lock($job_id) {
     300        global $wpdb;
     301        $table = self::table();
     302
     303        $claimed = $wpdb->query($wpdb->prepare(
     304            "UPDATE %i SET status = 'running', locked_by = %s, updated_at = %s
     305             WHERE id = %d AND status = 'pending'",
     306            $table,
     307            wp_generate_uuid4(),
     308            current_time('mysql'),
     309            $job_id
     310        ));
     311
     312        if (!$claimed) {
     313            return null;
     314        }
     315
     316        $job = $wpdb->get_row($wpdb->prepare(
     317            'SELECT * FROM %i WHERE id = %d',
     318            $table,
     319            $job_id
     320        ), ARRAY_A);
     321
     322        if ($job) {
     323            $job['payload'] = json_decode($job['payload'], true) ?: [];
     324        }
     325
     326        return $job;
     327    }
     328
    188329    private static function running_count() {
    189330        global $wpdb;
    190331        return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status = 'running'", self::table()));
     332    }
     333
     334    /** @return array<int,true> Hội thoại đang có job chạy (tối đa MAX_CONCURRENT dòng). */
     335    private static function running_conversations() {
     336        global $wpdb;
     337        $payloads = $wpdb->get_col($wpdb->prepare("SELECT payload FROM %i WHERE status = 'running'", self::table()));
     338
     339        $busy = [];
     340        foreach ((array) $payloads as $payload) {
     341            $conversation_id = self::payload_conversation($payload);
     342            if ($conversation_id > 0) {
     343                $busy[$conversation_id] = true;
     344            }
     345        }
     346        return $busy;
     347    }
     348
     349    private static function payload_conversation($payload) {
     350        $data = is_array($payload) ? $payload : json_decode((string) $payload, true);
     351        return is_array($data) ? (int) ($data['conversation_id'] ?? 0) : 0;
     352    }
     353
     354    private static function conversation_first(array $payload) {
     355        if (!array_key_exists('conversation_id', $payload)) {
     356            return $payload;
     357        }
     358        $payload['conversation_id'] = (int) $payload['conversation_id'];
     359        return ['conversation_id' => $payload['conversation_id']] + $payload;
    191360    }
    192361
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/MigrationManager.php

    r3752682 r3765552  
    1414     * 3 = đo lường 2.4.0: cột chuyển nhân viên và kết quả hội thoại, cột truy vết chi phí AI,
    1515     *     bảng value_events.
     16     * 4 = 2.6.0: conversations.language (ngôn ngữ khách đang viết, S2).
    1617     */
    17     const SCHEMA_REVISION = 3;
     18    const SCHEMA_REVISION = 4;
    1819
    1920    /** Kết quả lần chuyển dữ liệu gần nhất, để chẩn đoán và cho test đọc. */
    … …  
    6465        // Cài mới: model đề xuất (chỉ khi benchmark đã duyệt). Nâng cấp không bao giờ đổi model đã lưu.
    6566        \Smartbuy\SMBCommerce\AI\ModelPolicy::apply_new_install_defaults($previous_version);
     67
     68        // 2.6.0: cài mới trả lời theo ngôn ngữ khách; site nâng cấp giữ hành vi cũ (quyết định 3).
     69        \Smartbuy\SMBCommerce\AI\ReplyLanguage::apply_install_default($previous_version);
    6670
    6771        // Ngày bắt đầu đo của bảng "AI Value & Performance" (UTC): ghi một lần, không bao giờ ghi đè,
    … …  
    151155            'outcome_at'       => 'datetime DEFAULT NULL',
    152156        ]);
     157
     158        // 2.6.0 — ngôn ngữ khách đang viết (S2): chọn ngôn ngữ trả lời và ngôn ngữ nhận giọng nói.
     159        self::add_missing_columns($prefix . 'conversations', [
     160            'language' => 'varchar(16) DEFAULT NULL',
     161        ]);
    153162    }
    154163
    … …  
    208217                outcome varchar(30) DEFAULT NULL,
    209218                outcome_at datetime DEFAULT NULL,
     219                language varchar(16) DEFAULT NULL,
    210220                created_at datetime DEFAULT NULL,
    211221                updated_at datetime DEFAULT NULL,
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Plugin.php

    r3752682 r3765552  
    4545        }
    4646
     47        // Xin đánh giá WordPress.org khi plugin đã giúp được thật (2.6.0, S10).
     48        if (class_exists('\\Smartbuy\SMBCommerce\\Admin\\ReviewPrompt')) {
     49            \Smartbuy\SMBCommerce\Admin\ReviewPrompt::register();
     50        }
     51
    4752        // Domain-lock notice — chỉ hiển thị khi license.php tồn tại và domain không khớp.
    4853        // Bản phát triển (không có license.php) hoàn toàn im lặng.
    … …  
    7883            \Smartbuy\SMBCommerce\Core\JobDispatcher::init();
    7984        }
     85        if (class_exists('\\Smartbuy\SMBCommerce\\Core\\AsyncKick')) {
     86            \Smartbuy\SMBCommerce\Core\AsyncKick::init();
     87        }
    8088
    8189        if (class_exists('\\Smartbuy\SMBCommerce\\Core\\CronManager')) {
    … …  
    9098            \Smartbuy\SMBCommerce\Analytics\OrderAttribution::init();
    9199        }
     100
     101        // Abilities API (WordPress 6.9+) cho catalog công khai (2.6.0, S8); chủ shop bật tay.
     102        if (class_exists('\\Smartbuy\SMBCommerce\\Abilities\\AbilitiesService')) {
     103            \Smartbuy\SMBCommerce\Abilities\AbilitiesService::init();
     104        }
     105
     106        // Sales Insights (2.6.0, S6): lịch gán nhãn đêm và bản tin tuần (gói Starter).
     107        if (class_exists('\\Smartbuy\SMBCommerce\\Insights\\InsightsService')) {
     108            \Smartbuy\SMBCommerce\Insights\InsightsService::init();
     109        }
    92110    }
    93111}
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/PremiumFallbacks.php

    r3747521 r3765552  
    3030            'Smartbuy\SMBCommerce\\Commerce\\Woo\\VariationService' => NullVariationService::class,
    3131            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CouponService'    => NullCouponService::class,
     32            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CartActionService' => NullCartActionService::class,
    3233            'Smartbuy\SMBCommerce\\Integrations\\TelegramService'   => NullTelegramService::class,
     34            'Smartbuy\SMBCommerce\\Insights\\InsightsLabeler'       => NullInsightsLabeler::class,
     35            'Smartbuy\SMBCommerce\\Insights\\InsightsDigest'        => NullInsightsDigest::class,
     36            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CustomerCard'     => NullCustomerCard::class,
    3337        ];
    3438    }
    … …  
    127131}
    128132
     133/** Bản Free: không có hành động giỏ nâng cao (gói Starter); thẻ giữ nút "Choose options". */
     134class NullCartActionService {
     135    public static function enabled() { return false; }
     136    public static function enrich_card(array $card, $product) { return $card; }
     137    public static function propose(array $params) {
     138        return \Smartbuy\SMBCommerce\Commerce\CommerceResult::not_entitled('cart_actions_advanced');
     139    }
     140    public static function allowlist() { return []; }
     141    public static function prompt_block() { return ''; }
     142    public static function offers_coupons() { return false; }
     143    public static function widget_module_version() { return ''; }
     144    public static function widget_strings() { return []; }
     145}
     146
     147/** Bản Free: Sales Insights không có gán nhãn AI "vì sao không mua" (gói Starter); phần không AI vẫn chạy. */
     148class NullInsightsLabeler {
     149    const FEATURE = 'insights_ai';
     150    const CRON_HOOK = 'smbcommerce_insights_label';
     151    public static function allowed() { return false; }
     152    public static function enabled() { return false; }
     153    public static function run_nightly($mode = '') { return ['stopped' => 'not_entitled']; }
     154    public static function run($limit = 0, $now = null) { return ['stopped' => 'not_entitled']; }
     155    public static function status() {
     156        return ['allowed' => false, 'enabled' => false, 'spent' => 0.0, 'budget' => 0.0, 'model' => '', 'state' => [], 'next_run' => 0];
     157    }
     158    public static function choose_model() { return null; }
     159    public static function spent_this_month() { return 0.0; }
     160    public static function candidates($limit, $now = null) { return []; }
     161    public static function transcript($conversation_id) { return ['text' => '', 'customer' => '']; }
     162    public static function parse($text, $customer_text) { return null; }
     163}
     164
     165/** Bản Free: không có bản tin Sales Insights hằng tuần (gói Starter). */
     166class NullInsightsDigest {
     167    const CRON_HOOK = 'smbcommerce_insights_digest';
     168    public static function enabled() { return false; }
     169    public static function send_weekly() { return false; }
     170    public static function build($now = null) { return null; }
     171    public static function reason_label($reason) { return (string) $reason; }
     172}
     173
     174/** Bản Free: không có thẻ khách trong Inbox (gói Starter). */
     175class NullCustomerCard {
     176    const FEATURE = 'customer_card';
     177    public static function allowed() { return false; }
     178    public static function viewer_can_see_orders() { return false; }
     179    public static function for_conversation($conversation_id) { return null; }
     180}
     181
    129182/** Bản Free: không có thông báo Telegram (gói Starter); lead vẫn được lưu bình thường. */
    130183class NullTelegramService {
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Uninstaller.php

    r3752682 r3765552  
    5454        wp_clear_scheduled_hook('smbcommerce_run_jobs');
    5555        wp_clear_scheduled_hook('smbcommerce_finalize_outcomes');
     56        \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule();
    5657
    5758        /*
    … …  
    7778        delete_option('smbcommerce_measurement_started_at');
    7879        delete_option('smbcommerce_outcomes_watermark');
     80        delete_option('smbcommerce_ai_max_concurrency');
     81        delete_option('smbcommerce_async_mode');
     82        delete_option('smbcommerce_async_health');
    7983        delete_transient('smbcommerce_missing_key_notice');
     84        delete_option('smbcommerce_review_prompt');
     85        delete_transient('smbcommerce_wp_ai_available');
    8086    }
    8187
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Licensing/LicenseGate.php

    r3752682 r3765552  
    4545        // endpoint chỉ trả trường doanh thu khi can('revenue_attribution').
    4646        'revenue_attribution'  => self::TIER_STARTER,
     47        // Giỏ trong chat nâng cao (2.6.0, S4): chọn biến thể trên thẻ, số lượng, xoá, mã giảm giá cho phép.
     48        'cart_actions_advanced' => self::TIER_STARTER,
     49        // Sales Insights có AI (2.6.0, S6): gán nhãn lý do không mua, bản tin tuần. Phần không AI là Free.
     50        'insights_ai'           => self::TIER_STARTER,
     51        // Thẻ khách trong Inbox (2.6.0, S12): đơn, tổng chi, sản phẩm gần đây của khách đã biết chắc.
     52        'customer_card'         => self::TIER_STARTER,
    4753
    4854        // --- Pro ---
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Privacy/PersonalData.php

    r3752682 r3765552  
    185185        }
    186186
     187        $E = '\\Smartbuy\\SMBCommerce\\Analytics\\ValueEvents';
    187188        $events = $wpdb->get_results($wpdb->prepare(
    188189            'SELECT id, event_type, conversation_id, order_id, meta, created_at FROM %i
    189              WHERE event_type IN (%s, %s) AND conversation_id IN (' . implode(',', array_fill(0, count($conversation_ids), '%d')) . ')
     190             WHERE event_type IN (%s, %s, %s, %s, %s) AND conversation_id IN (' . implode(',', array_fill(0, count($conversation_ids), '%d')) . ')
    190191             ORDER BY id ASC',
    191192            array_merge(
    192                 [self::table('value_events'), \Smartbuy\SMBCommerce\Analytics\ValueEvents::ORDER_LINKED, \Smartbuy\SMBCommerce\Analytics\ValueEvents::RESOLUTION_ANSWER],
     193                // Sales Insights (2.6.0): câu hỏi đã che và trích dẫn đã che cũng là chữ của người này.
     194                // Thẻ khách (2.6.0, S12): hội thoại gắn với tài khoản đã đăng nhập hoặc đơn đã chứng minh sở hữu.
     195                [self::table('value_events'), $E::ORDER_LINKED, $E::RESOLUTION_ANSWER, $E::UNANSWERED, $E::NO_PURCHASE_REASON, $E::CUSTOMER_VERIFIED],
    193196                $conversation_ids
    194197            )
    … …  
    198201        foreach ($events as $event) {
    199202            $meta = json_decode((string) $event['meta'], true) ?: [];
    200             $is_order = $event['event_type'] === \Smartbuy\SMBCommerce\Analytics\ValueEvents::ORDER_LINKED;
     203            switch ($event['event_type']) {
     204                case $E::ORDER_LINKED:
     205                    $row = ['name' => __('Order linked to this chat', 'ninhle-ai-sales-assistant'), 'value' => $event['order_id']];
     206                    break;
     207                case $E::UNANSWERED:
     208                    $row = ['name' => __('Question the assistant could not answer (contact details hidden)', 'ninhle-ai-sales-assistant'), 'value' => $meta['question_norm'] ?? ''];
     209                    break;
     210                case $E::CUSTOMER_VERIFIED:
     211                    $row = ['name' => __('Customer identified in this chat', 'ninhle-ai-sales-assistant'), 'value' => !empty($meta['user_id'])
     212                        /* translators: %d: WordPress user ID of the logged-in customer. */
     213                        ? sprintf(__('Logged-in account #%d', 'ninhle-ai-sales-assistant'), (int) $meta['user_id'])
     214                        /* translators: %d: WooCommerce order ID whose ownership the customer proved in the chat. */
     215                        : sprintf(__('Order ID %d (ownership checked)', 'ninhle-ai-sales-assistant'), (int) ($meta['order_id'] ?? 0))];
     216                    break;
     217                case $E::NO_PURCHASE_REASON:
     218                    $row = ['name' => __('Reason the chat did not lead to an order (AI label)', 'ninhle-ai-sales-assistant'),
     219                        'value' => trim(($meta['reason'] ?? '') . (empty($meta['quote']) ? '' : ': "' . $meta['quote'] . '"'))];
     220                    break;
     221                default:
     222                    $row = ['name' => __('Answer to "Did this answer your question?"', 'ninhle-ai-sales-assistant'), 'value' => $meta['answer'] ?? ''];
     223            }
    201224            $items[] = [
    202225                'group_id' => 'smb-commerce-value',
    … …  
    205228                'data' => [
    206229                    ['name' => __('Conversation', 'ninhle-ai-sales-assistant'), 'value' => $event['conversation_id']],
    207                     $is_order
    208                         ? ['name' => __('Order linked to this chat', 'ninhle-ai-sales-assistant'), 'value' => $event['order_id']]
    209                         : ['name' => __('Answer to "Did this answer your question?"', 'ninhle-ai-sales-assistant'), 'value' => $meta['answer'] ?? ''],
     230                    $row,
    210231                    ['name' => __('Recorded (UTC)', 'ninhle-ai-sales-assistant'), 'value' => $event['created_at']],
    211232                ],
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/AdminController.php

    r3761175 r3765552  
    109109    }
    110110   
     111    /** Một hội thoại (Inbox mở từ Sales Insights). */
     112    public static function get_conversation($request) {
     113        $conversation = ConversationRepository::get_conversation((int) $request->get_param('id'));
     114        if (!$conversation) {
     115            return new \WP_Error('not_found', 'Conversation not found', ['status' => 404]);
     116        }
     117        return rest_ensure_response($conversation);
     118    }
     119
     120    /**
     121     * Thẻ khách của hội thoại (S12, Starter): chỉ khi khách đã biết chắc và người xem có quyền xem đơn
     122     * WooCommerce. Không có gói hoặc không có quyền thì card = null (giao diện không hiện cột).
     123     */
     124    public static function get_customer_card($request) {
     125        $cards = \Smartbuy\SMBCommerce\Commerce\Woo\CustomerCard::class;
     126        if (!$cards::allowed()) {
     127            return rest_ensure_response(['allowed' => false, 'card' => null]);
     128        }
     129        if (!$cards::viewer_can_see_orders()) {
     130            return rest_ensure_response(['allowed' => true, 'can_view_orders' => false, 'card' => null]);
     131        }
     132        return rest_ensure_response([
     133            'allowed' => true,
     134            'can_view_orders' => true,
     135            'card' => $cards::for_conversation((int) $request->get_param('id')),
     136        ]);
     137    }
     138
    111139    public static function delete_conversation($request) {
    112140        if (!current_user_can('manage_smbcommerce')) {
    … …  
    170198        $settings['smbcommerce_font_family'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_font_family', 'system');
    171199        $settings['smbcommerce_ai_provider'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_ai_provider', 'openai');
     200        // Abilities API cho AI agent (S8): WordPress 6.9+.
     201        $settings['smbcommerce_abilities_enabled'] = \Smartbuy\SMBCommerce\Abilities\AbilitiesService::enabled() ? '1' : '0';
     202        $settings['smbcommerce_abilities_supported'] = \Smartbuy\SMBCommerce\Abilities\AbilitiesService::supported();
     203        // Kết nối AI của WordPress 7.0+ (S7): chỉ báo có/không, không bao giờ trả khoá.
     204        $settings['smbcommerce_wp_ai_available'] = \Smartbuy\SMBCommerce\AI\Providers\WordPressAiProvider::available(true);
    172205       
    173206        $settings['smbcommerce_openai_key_exists'] = !empty(\Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_openai_key'));
    … …  
    232265        $settings['smbcommerce_hide_on_checkout'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_hide_on_checkout', '0');
    233266        $settings['smbcommerce_voice_input_enabled'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_voice_input_enabled', '0');
     267        $settings['smbcommerce_reply_language'] = \Smartbuy\SMBCommerce\AI\ReplyLanguage::mode();
     268        $settings['smbcommerce_coupon_allowlist'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_coupon_allowlist', '');
     269        // Mã vận đơn theo meta riêng của plugin vận chuyển (S5); trải ra thành các ô của màn hình cài đặt.
     270        $tracking = \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::map() ?: [];
     271        foreach (['code_meta', 'carrier_meta', 'carrier', 'url_template'] as $field) {
     272            $settings['smbcommerce_tracking_' . $field] = (string) ($tracking[$field] ?? '');
     273        }
    234274
    235275        return rest_ensure_response(['settings' => $settings]);
    … …  
    280320            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_font_family', sanitize_text_field($params['smbcommerce_font_family']));
    281321        }
    282         if (isset($params['smbcommerce_ai_provider'])) {
    283             \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_provider', sanitize_text_field($params['smbcommerce_ai_provider']));
     322        // Chỉ nhận provider plugin biết (thêm 'wordpress' từ 2.6.0, S7); giá trị lạ bị bỏ qua.
     323        if (isset($params['smbcommerce_ai_provider'])
     324            && in_array($params['smbcommerce_ai_provider'], ['openai', 'anthropic', 'gemini', 'wordpress'], true)) {
     325            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_provider', $params['smbcommerce_ai_provider']);
    284326        }
    285327       
    … …  
    397439            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_telegram_link', sanitize_text_field($params['smbcommerce_telegram_link']));
    398440        }
    399         foreach (['smbcommerce_ai_notice_enabled', 'smbcommerce_hide_on_checkout', 'smbcommerce_voice_input_enabled'] as $toggle) {
     441        foreach (['smbcommerce_ai_notice_enabled', 'smbcommerce_hide_on_checkout', 'smbcommerce_voice_input_enabled', 'smbcommerce_abilities_enabled'] as $toggle) {
    400442            if (isset($params[$toggle])) {
    401443                \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting($toggle, (string) $params[$toggle] === '1' ? '1' : '0');
    … …  
    405447            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_notice_text', sanitize_text_field($params['smbcommerce_ai_notice_text']));
    406448        }
     449        // Chỉ hai giá trị; giá trị lạ không được lưu (sẽ bị đọc thành 'customer' và đổi hành vi site nâng cấp).
     450        if (isset($params['smbcommerce_reply_language'])
     451            && in_array($params['smbcommerce_reply_language'], [\Smartbuy\SMBCommerce\AI\ReplyLanguage::MODE_CUSTOMER, \Smartbuy\SMBCommerce\AI\ReplyLanguage::MODE_STORE], true)) {
     452            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_reply_language', $params['smbcommerce_reply_language']);
     453        }
     454
     455        // Starter (S4): mã AI được đề xuất, mỗi dòng "MÃ" hoặc "MÃ, mức giảm tối đa"; tối đa 20 dòng.
     456        if (isset($params['smbcommerce_coupon_allowlist'])) {
     457            $lines = preg_split('/\R/', sanitize_textarea_field((string) $params['smbcommerce_coupon_allowlist']));
     458            $lines = array_slice(array_values(array_filter(array_map('trim', $lines), 'strlen')), 0, 20);
     459            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_coupon_allowlist', implode("\n", $lines));
     460        }
     461
     462        // Mã vận đơn (S5): bốn ô gộp thành một cấu hình đã kiểm; khoá meta sai hoặc trống thì xoá cấu hình.
     463        if (isset($params['smbcommerce_tracking_code_meta'])) {
     464            $map = \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::sanitize_map([
     465                'code_meta' => (string) $params['smbcommerce_tracking_code_meta'],
     466                'carrier_meta' => (string) ($params['smbcommerce_tracking_carrier_meta'] ?? ''),
     467                'carrier' => (string) ($params['smbcommerce_tracking_carrier'] ?? ''),
     468                'url_template' => (string) ($params['smbcommerce_tracking_url_template'] ?? ''),
     469            ]);
     470            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting(\Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::SETTING_MAP, $map ?: '');
     471        }
     472
     473        // Kiểm lại cách chạy nền mỗi lần lưu (như Site Health): host có thể vừa mở hoặc chặn loopback.
     474        \Smartbuy\SMBCommerce\Core\AsyncKick::daily_check();
    407475
    408476        return rest_ensure_response(['success' => true]);
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/ChatController.php

    r3752682 r3765552  
    1313
    1414class ChatController {
    15    
     15
     16    /** Ý định Local dùng giỏ hàng của khách (LocalHandler::checkout, view_cart): chạy trong poll của khách. */
     17    const VISITOR_INTENTS = ['checkout', 'view_cart'];
     18
    1619    public static function bootstrap($request) {
    1720        $session_token = $request->get_header('X-SMB-Commerce-Session');
    … …  
    4043            $response['status'] = $conversation['status'];
    4144            $response['agent_name'] = self::agent_name($conversation);
     45            $response['language'] = $conversation['language'] ?? null;
    4246           
    4347            $messages = MessageRepository::get_messages($conversation['id'], 0, 50);
    … …  
    108112        }
    109113
     114        // Ngôn ngữ khách đang viết (S2): lưu khi đổi, dùng cho câu trả lời và nhập giọng nói.
     115        \Smartbuy\SMBCommerce\AI\ReplyLanguage::observe($conversation, $content);
     116
     117        // Khách đã đăng nhập (cookie + nonce REST): bằng chứng cho thẻ khách trong Inbox (S12).
     118        \Smartbuy\SMBCommerce\Commerce\CustomerProof::logged_in($conversation_id, get_current_user_id());
     119
    110120        // Hội thoại thật bắt đầu ở tin đầu tiên của khách (mở widget không tính); ghi một lần.
    111121        \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
    … …  
    121131        $is_thinking_mode = rest_sanitize_boolean($request->get_param('thinking_mode'));
    122132
    123         \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE, [
     133        $payload = [
    124134            'conversation_id' => (int) $conversation_id,
    125135            'thinking_mode' => (bool) $is_thinking_mode,
    126         ]);
     136            // Khách đã đăng nhập (cookie + nonce): worker nền không có người dùng hiện tại.
     137            'wp_user_id' => (int) get_current_user_id(),
     138        ];
     139
     140        // Câu về giỏ hàng được trả lời tại chỗ, không gọi AI, nhưng cần cookie phiên của khách:
     141        // để lượt poll kế tiếp của chính khách chạy (ngay sau ~100 ms). Mọi câu khác chạy nền.
     142        $routing = \Smartbuy\SMBCommerce\Commerce\LocalResolver::classify(
     143            $content,
     144            \Smartbuy\SMBCommerce\Commerce\RuntimeMode::current()
     145        );
     146        $visitor = \Smartbuy\SMBCommerce\Commerce\LocalResolver::is_local($routing)
     147            && in_array($routing['intent'] ?? '', self::VISITOR_INTENTS, true);
     148
     149        $strategy = \Smartbuy\SMBCommerce\Core\AsyncKick::strategy();
     150        if ($visitor) {
     151            $payload['visitor'] = true;
     152        } else {
     153            $payload['kick'] = $strategy;
     154        }
     155
     156        $job_id = \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE, $payload);
     157
     158        if (!$visitor && $job_id > 0) {
     159            \Smartbuy\SMBCommerce\Core\AsyncKick::kick($job_id, $strategy);
     160        }
    127161
    128162        return rest_ensure_response($message);
    … …  
    145179        }
    146180
    147         // Request poll này chính là worker của hàng đợi. Nhờ vậy câu trả lời luôn
    148         // được sinh ra mà không cần cron chạy đúng giờ hay loopback hoạt động.
    149         \Smartbuy\SMBCommerce\Core\JobDispatcher::run_one();
     181        // 2.6.0: AI chạy nền, poll chỉ đọc. Nó chỉ tự chạy job của CHÍNH hội thoại này khi job cần
     182        // phiên của khách, khi chạy nền bị host chặn, hoặc khi nền chưa nhận sau thời gian ân hạn.
     183        $turn = \Smartbuy\SMBCommerce\Core\JobDispatcher::poll_turn($conversation_id);
     184
     185        // Tương tác có sản phẩm từ lượt AI chạy nền: giờ mới ghi được vào phiên của khách.
     186        \Smartbuy\SMBCommerce\Analytics\OrderAttribution::flush_deferred($conversation_id);
    150187
    151188        $messages = MessageRepository::get_messages($conversation_id, $last_message_id, 50);
    152189
    153         // Trạng thái đọc lại SAU run_one: lượt xử lý vừa chạy có thể đã chuyển nhân viên.
    154         $conversation = ConversationRepository::get_conversation($conversation_id) ?: $conversation;
     190        // Lượt xử lý vừa chạy ở đây có thể đã chuyển nhân viên: đọc lại trạng thái.
     191        if ($turn['ran']) {
     192            $conversation = ConversationRepository::get_conversation($conversation_id) ?: $conversation;
     193        }
     194
     195        // queued: hết slot chạy đồng thời. Không còn job nào thì không cần đọc giai đoạn.
     196        if ($turn['queued']) {
     197            $stage = 'queued';
     198        } elseif ($turn['pending'] > 0) {
     199            $stage = \Smartbuy\SMBCommerce\AI\AIService::current_stage($conversation_id);
     200        } else {
     201            $stage = null;
     202        }
    155203
    156204        return rest_ensure_response([
    … …  
    160208            'status' => $conversation['status'],
    161209            'agent_name' => self::agent_name($conversation),
    162             'stage' => \Smartbuy\SMBCommerce\AI\AIService::current_stage($conversation_id),
     210            'stage' => $stage,
     211            // Nhập giọng nói nhận theo ngôn ngữ khách đang viết, trước <html lang> của trang.
     212            'language' => $conversation['language'] ?? null,
    163213        ]);
    164214    }
    … …  
    236286
    237287        if ($result['changed']) {
    238             MessageRepository::add_message($conversation_id, 'system', __('Thanks, our team has been notified. A member of staff will reply here as soon as possible.', 'ninhle-ai-sales-assistant'));
     288            \Smartbuy\SMBCommerce\AI\ReplyLanguage::in_conversation_language($conversation_id, function () use ($conversation_id) {
     289                MessageRepository::add_message($conversation_id, 'system', __('Thanks, our team has been notified. A member of staff will reply here as soon as possible.', 'ninhle-ai-sales-assistant'));
     290            });
    239291
    240292            \Smartbuy\SMBCommerce\Webhooks\WebhookDispatcher::send(
    … …  
    273325
    274326    /** Loại sự kiện widget được gửi lên; mọi giá trị khác bị từ chối. */
    275     const WIDGET_EVENTS = ['product_click', 'cart_click', 'resolution_yes', 'resolution_no'];
     327    const WIDGET_EVENTS = ['product_click', 'cart_click', 'cart_add', 'resolution_yes', 'resolution_no'];
    276328
    277329    /**
    … …  
    310362                ['answer' => $type === 'resolution_yes' ? 'yes' : 'no']
    311363            );
     364            // Sales Insights (S6): câu khách vừa hỏi là câu chưa được giải đáp.
     365            if ($recorded && $type === 'resolution_no') {
     366                \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'customer_no');
     367            }
    312368            return rest_ensure_response(['recorded' => $recorded]);
    313369        }
    … …  
    318374        }
    319375        $ref = substr(preg_replace('/[^a-z0-9_-]/i', '', (string) $request->get_param('ref')), 0, 64);
     376
     377        // Thêm vào giỏ trong chat (S4): widget báo SAU khi Store API đã thêm thành công. Một sản phẩm tính
     378        // một lần mỗi hội thoại cho tỉ lệ "chat → thêm vào giỏ".
     379        if ($type === 'cart_add' && ctype_digit($ref)) {
     380            \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
     381                \Smartbuy\SMBCommerce\Analytics\ValueEvents::CART_ADD,
     382                $conversation_id,
     383                'p:' . $ref,
     384                'customer',
     385                null,
     386                ['product_id' => (int) $ref, 'qty' => max(1, min(99, (int) $request->get_param('qty')))]
     387            );
     388        }
     389
    320390        $recorded = \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
    321391            \Smartbuy\SMBCommerce\Analytics\ValueEvents::QUALIFYING_INTERACTION,
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/RouteRegistry.php

    r3752682 r3765552  
    99
    1010class RouteRegistry {
     11    /** @var float|null Lúc handler của plugin bắt đầu, cho header Server-Timing. */
     12    private static $started = null;
     13
    1114    public static function init() {
    1215        add_action('rest_api_init', [self::class, 'register_routes']);
     16        add_filter('rest_request_before_callbacks', [self::class, 'start_timer'], 10, 3);
    1317        add_filter('rest_post_dispatch', [self::class, 'no_cache_headers'], 10, 3);
     18    }
     19
     20    public static function start_timer($response, $handler, $request) {
     21        if (strpos((string) $request->get_route(), '/smb-commerce/v1/') === 0) {
     22            self::$started = microtime(true);
     23        }
     24        return $response;
    1425    }
    1526
    … …  
    2233     */
    2334    public static function no_cache_headers($response, $server, $request) {
    24         if (!($response instanceof \WP_REST_Response) || strpos((string) $request->get_route(), '/smb-commerce/v1/') !== 0) {
     35        if (!($response instanceof \WP_REST_Response)) {
     36            return $response;
     37        }
     38        $route = (string) $request->get_route();
     39        if (strpos($route, '/smb-commerce/v1/') !== 0 && !self::is_store_cart_route($route)) {
    2540            return $response;
    2641        }
    … …  
    3247        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- API công khai của plugin LiteSpeed Cache, phải gọi đúng tên của họ.
    3348        do_action('litespeed_control_set_nocache', 'Ninhle AI Sales Assistant REST response');
     49
     50        // Thời gian của riêng handler plugin (không gồm lúc WordPress khởi động), xem được trong
     51        // DevTools > Network > Timing; đo poll nhẹ của S1 (2.6.0) trên host thật.
     52        if (self::$started !== null && strpos((string) $request->get_route(), '/smb-commerce/v1/chat/') === 0) {
     53            // wp = cả request PHP từ lúc máy chủ nhận (gồm khởi động WordPress), để biết chi phí thật của mỗi poll.
     54            $total = isset($_SERVER['REQUEST_TIME_FLOAT']) ? (microtime(true) - (float) $_SERVER['REQUEST_TIME_FLOAT']) * 1000 : 0;
     55            $response->header('Server-Timing', sprintf('smb;dur=%.1f, wp;dur=%.1f', (microtime(true) - self::$started) * 1000, $total));
     56        }
    3457        return $response;
     58    }
     59
     60    /**
     61     * Route giỏ hàng của WooCommerce Store API (/wc/store/v1/cart, …/cart/items…). Giỏ là của RIÊNG từng
     62     * khách nhưng LiteSpeed Cache mặc định cache cả REST GET: trên staging (2026-10-24) GET /wc/store/v1/cart
     63     * trả "hit, public, max-age 7 ngày" bất kể cookie, nên khách thấy giỏ rỗng hoặc giỏ của người khác và
     64     * nhận nonce cũ (hết hạn sau ~1 ngày, "Add to cart" trong chat sẽ lỗi). Route sản phẩm công khai không đụng.
     65     */
     66    public static function is_store_cart_route($route) {
     67        return (bool) preg_match('#^/wc/store(/v\d+)?/cart(/|$)#', (string) $route);
    3568    }
    3669
    … …  
    70103        ]);
    71104
     105        // Worker nền qua loopback (AsyncKick, 2.6.0). Không có phiên hay cookie: quyền là chữ ký
     106        // HMAC gắn với ID job và hạn 60 giây; job 0 là lời gọi kiểm tra loopback.
     107        register_rest_route($namespace, \Smartbuy\SMBCommerce\Core\AsyncKick::ROUTE, [
     108            'methods'  => \WP_REST_Server::CREATABLE,
     109            'callback' => [\Smartbuy\SMBCommerce\Core\AsyncKick::class, 'handle'],
     110            'permission_callback' => [\Smartbuy\SMBCommerce\Core\AsyncKick::class, 'permission'],
     111            'args' => [
     112                'job' => ['type' => 'integer', 'required' => true, 'minimum' => 0],
     113                'exp' => ['type' => 'integer', 'required' => true, 'minimum' => 0],
     114                'sig' => ['type' => 'string', 'required' => true, 'pattern' => '^[a-f0-9]{64}$'],
     115            ],
     116        ]);
     117
    72118        register_rest_route($namespace, '/chat/clear', [
    73119            'methods'  => \WP_REST_Server::CREATABLE,
    … …  
    98144                'type' => Guard::arg_text(true, 32),
    99145                'ref' => Guard::arg_text(false, 64),
     146                // Số lượng của sự kiện cart_add (2.6.0, S4).
     147                'qty' => ['type' => 'integer', 'required' => false, 'minimum' => 1, 'maximum' => 99],
    100148            ],
    101149        ]);
    … …  
    263311        ]);
    264312
     313        // Tab "Sales Insights" (2.6.0, S6; phần AI kiểm gói trong handler).
     314        register_rest_route($namespace, '/admin/insights', [
     315            'methods'  => \WP_REST_Server::READABLE,
     316            'callback' => [InsightsController::class, 'report'],
     317            'permission_callback' => [self::class, 'check_manage'],
     318        ]);
     319        foreach (['answer' => 'answer', 'dismiss' => 'dismiss', 'notes/delete' => 'delete_note', 'settings' => 'save_settings'] as $path => $method) {
     320            register_rest_route($namespace, '/admin/insights/' . $path, [
     321                'methods'  => \WP_REST_Server::CREATABLE,
     322                'callback' => [InsightsController::class, $method],
     323                'permission_callback' => [self::class, 'check_manage'],
     324            ]);
     325        }
     326        // Mở một hội thoại từ Sales Insights trong Inbox.
     327        register_rest_route($namespace, '/admin/conversations/(?P<id>\d+)', [
     328            'methods'  => \WP_REST_Server::READABLE,
     329            'callback' => [AdminController::class, 'get_conversation'],
     330            'permission_callback' => [self::class, 'check_manage'],
     331        ]);
     332        // Thẻ khách trong Inbox (S12, Starter); handler kiểm thêm quyền xem đơn WooCommerce.
     333        register_rest_route($namespace, '/admin/conversations/(?P<id>\d+)/customer', [
     334            'methods'  => \WP_REST_Server::READABLE,
     335            'callback' => [AdminController::class, 'get_customer_card'],
     336            'permission_callback' => [self::class, 'check_view_inbox'],
     337        ]);
     338
    265339        // Bảng "AI Value & Performance" (mọi gói; doanh thu kiểm quyền trong handler).
    266340        register_rest_route($namespace, '/admin/value-metrics', [
    … …  
    317391            '/tools/product-variations' => 'get_product_variations',
    318392            '/tools/check-coupon'       => 'check_coupon',
     393            '/tools/propose-cart-action' => 'propose_cart_action',
    319394            '/tools/order-status'       => 'get_order_status',
     395            '/tools/my-orders'          => 'get_my_orders',
    320396            '/tools/customer-downloads' => 'get_customer_downloads',
    321397            '/tools/policy'             => 'get_policy',
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/ToolController.php

    r3747521 r3765552  
    5959    }
    6060
     61    /**
     62     * Đề xuất hành động giỏ (Starter, S4): sản phẩm kèm biến thể/số lượng, hoặc mã giảm giá trong danh sách
     63     * cho phép. Không ghi giỏ: chỉ kiểm và dựng thẻ để khách tự bấm.
     64     */
     65    public static function propose_cart_action($request) {
     66        // Lựa chọn bằng chữ ("Size" => "M"): tối đa 5 thuộc tính, chữ trơn.
     67        $options = [];
     68        foreach (array_slice((array) ($request->get_param('options') ?: []), 0, 5, true) as $name => $value) {
     69            if (is_scalar($value)) {
     70                $options[sanitize_text_field((string) $name)] = sanitize_text_field((string) $value);
     71            }
     72        }
     73        return self::respond_flat(\Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::propose([
     74            'type' => sanitize_key((string) $request->get_param('type')),
     75            'product_id' => (int) $request->get_param('product_id'),
     76            'variation_id' => (int) $request->get_param('variation_id'),
     77            'options' => $options,
     78            'quantity' => (int) ($request->get_param('quantity') ?: 1),
     79            'code' => sanitize_text_field((string) $request->get_param('code')),
     80        ]));
     81    }
     82
    6183    public static function get_product_detail($request) {
    6284        $service = new \Smartbuy\SMBCommerce\Commerce\Woo\ProductService();
    … …  
    87109        ];
    88110
    89         // Khách đã đăng nhập thì chính phiên WordPress là bằng chứng.
    90         if (function_exists('get_current_user_id') && get_current_user_id() > 0) {
    91             $proof['user_id'] = get_current_user_id();
     111        // Khách đã đăng nhập thì chính phiên WordPress là bằng chứng: lấy từ khách của lượt (job chạy nền
     112        // không có người dùng của khách).
     113        $customer = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id();
     114        if ($customer > 0) {
     115            $proof['user_id'] = $customer;
    92116        }
    93117
    … …  
    95119
    96120        return self::respond($service->lookup($reference, $proof), 'order');
     121    }
     122
     123    /**
     124     * Đơn gần nhất của khách đã đăng nhập (S5). Người dùng lấy từ khách của lượt, không từ tham số: model
     125     * không chọn được tài khoản nào để xem.
     126     */
     127    public static function get_my_orders($request) {
     128        $service = new \Smartbuy\SMBCommerce\Commerce\Woo\OrderService();
     129
     130        return self::respond_flat($service->recent_for_customer(\Smartbuy\SMBCommerce\Support\CustomerContext::user_id(), 5));
    97131    }
    98132
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Widget/WidgetLoader.php

    r3761175 r3765552  
    6363            'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')),
    6464            'nonce' => wp_create_nonce('wp_rest'),
     65            // Khách đã đăng nhập (S5): widget gửi nonce kèm tin nhắn để máy chủ biết tài khoản (đơn của tôi).
     66            // Trang của người đăng nhập thường không bị cache trang; nonce sai thì widget gửi lại như khách vãng lai.
     67            'loggedIn' => function_exists('is_user_logged_in') && is_user_logged_in(),
    6568            'widgetConfig' => self::public_config(),
    6669            // Dịch ở phía PHP để chuỗi vào được file .pot và theo locale WordPress.
    6770            'i18n' => [
    6871                'openChat'           => __('Open chat', 'ninhle-ai-sales-assistant'),
     72                /* translators: %d: number of replies the shopper has not read yet. */
     73                'unreadMessages'     => __('new replies: %d', 'ninhle-ai-sales-assistant'),
    6974                'chatWindow'         => __('Chat with our assistant', 'ninhle-ai-sales-assistant'),
    7075                'cart'               => __('Cart', 'ninhle-ai-sales-assistant'),
    … …  
    109114                'stageOrder'         => __('Looking up your order…', 'ninhle-ai-sales-assistant'),
    110115                'stageHandoff'       => __('Connecting you with a team member…', 'ninhle-ai-sales-assistant'),
     116                'stageQueued'        => __('Waiting in line…', 'ninhle-ai-sales-assistant'),
     117                // Thẻ sản phẩm (2.6.0, S3).
     118                'productsLabel'      => __('Products', 'ninhle-ai-sales-assistant'),
     119                'cardsPrev'          => __('Previous products', 'ninhle-ai-sales-assistant'),
     120                'cardsNext'          => __('Next products', 'ninhle-ai-sales-assistant'),
     121                'cardView'           => __('View', 'ninhle-ai-sales-assistant'),
     122                'stockIn'            => __('In stock', 'ninhle-ai-sales-assistant'),
     123                'stockOut'           => __('Out of stock', 'ninhle-ai-sales-assistant'),
     124                'stockBackorder'     => __('On backorder', 'ninhle-ai-sales-assistant'),
     125                /* translators: %s: date the price was shown, e.g. "24 Oct 2026". */
     126                'priceAt'            => __('Price on %s', 'ninhle-ai-sales-assistant'),
     127                // Giỏ hàng trong chat (2.6.0, S4).
     128                'addToCart'          => __('Add to cart', 'ninhle-ai-sales-assistant'),
     129                'adding'             => __('Adding…', 'ninhle-ai-sales-assistant'),
     130                'chooseOptions'      => __('Choose options', 'ninhle-ai-sales-assistant'),
     131                'addedToCart'        => __('Added to cart', 'ninhle-ai-sales-assistant'),
     132                'viewCart'           => __('View cart', 'ninhle-ai-sales-assistant'),
     133                'checkout'           => __('Checkout', 'ninhle-ai-sales-assistant'),
     134                'cartOutOfStock'     => __('Sorry, this item just sold out.', 'ninhle-ai-sales-assistant'),
     135                'cartLimit'          => __("You can't add more of this item.", 'ninhle-ai-sales-assistant'),
     136                'cartError'          => __("Couldn't add to cart.", 'ninhle-ai-sales-assistant'),
     137                'openProduct'        => __('Open the product page', 'ninhle-ai-sales-assistant'),
     138                'yourCart'           => __('Your cart', 'ninhle-ai-sales-assistant'),
     139                'closeCart'          => __('Close', 'ninhle-ai-sales-assistant'),
     140                'loadingCart'        => __('Loading your cart…', 'ninhle-ai-sales-assistant'),
     141                'cartLoadError'      => __("Couldn't load your cart.", 'ninhle-ai-sales-assistant'),
     142                'cartEmpty'          => __('Your cart is empty.', 'ninhle-ai-sales-assistant'),
     143                'subtotal'           => __('Subtotal', 'ninhle-ai-sales-assistant'),
     144                'discount'           => __('Discount', 'ninhle-ai-sales-assistant'),
     145                // Thẻ đơn hàng (2.6.0, S5).
     146                'ordersLabel'        => __('Your orders', 'ninhle-ai-sales-assistant'),
     147                /* translators: %s: order number. */
     148                'orderTitle'         => __('Order #%s', 'ninhle-ai-sales-assistant'),
     149                /* translators: %d: number of other items in the order. */
     150                'orderMore'          => __('and %d more', 'ninhle-ai-sales-assistant'),
     151                'orderTracking'      => __('Tracking number', 'ninhle-ai-sales-assistant'),
     152                'orderTrack'         => __('Track', 'ninhle-ai-sales-assistant'),
     153                'orderView'          => __('View order', 'ninhle-ai-sales-assistant'),
    111154                'resolutionQuestion' => __('Did this answer your question?', 'ninhle-ai-sales-assistant'),
    112155                'yes'                => __('Yes', 'ninhle-ai-sales-assistant'),
    … …  
    125168                'voiceLanguage'      => __('Your browser cannot recognise this language. Please type your message.', 'ninhle-ai-sales-assistant'),
    126169                'voiceUnavailable'   => __('Voice input is not available right now. Please type your message.', 'ninhle-ai-sales-assistant'),
    127             ],
     170            // Chuỗi của module giao diện Starter (S4); bản Free: mảng rỗng.
     171            ] + \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::widget_strings(),
    128172        ];
    129173
    … …  
    160204            // Giỏ hàng chỉ có khi WooCommerce đang chạy.
    161205            'cartUrl' => function_exists('wc_get_cart_url') ? wc_get_cart_url() : '',
     206            'checkoutUrl' => function_exists('wc_get_checkout_url') ? wc_get_checkout_url() : '',
     207            // Thêm vào giỏ trong chat (2.6.0, S4): trình duyệt của khách gọi Store API của WooCommerce; máy chủ
     208            // plugin không ghi giỏ. Rỗng = dùng link ?add-to-cart= như trước.
     209            'storeApi' => function_exists('WC') && function_exists('rest_url') ? rest_url('wc/store/v1/') : '',
     210            // Module giao diện Starter (biến thể trên thẻ, số lượng, mã giảm giá): phiên bản để widget tải
     211            // widget-pro.bundle.js cạnh bundle của nó; rỗng ở bản Free hoặc khi gói chưa mở.
     212            'proModule' => \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::widget_module_version(),
    162213            // Thông báo "đang trò chuyện với AI": mặc định bật; rỗng = tắt.
    163214            'aiNotice' => $get('smbcommerce_ai_notice_enabled', '1') === '1'
  • ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/WooCommerce/WooCommerceProductScanner.php

    r3761175 r3765552  
    3434
    3535        $synced_ids = [];
     36        $fetched = 0;
    3637        $page = 1;
    3738
    … …  
    5253
    5354            foreach ($products as $product) {
     55                // Sản phẩm "Ẩn" (Catalog visibility: Hidden) không vào cache: chủ shop không muốn nó hiện trong
     56                // cửa hàng hay kết quả tìm kiếm, và thẻ sản phẩm (2.6.0, S3) cũng bỏ qua nó. Trước đây AI vẫn tìm
     57                // thấy và giới thiệu (staging 25/10). Lần quét toàn bộ dọn bản đã cache.
     58                if (method_exists($product, 'get_catalog_visibility') && $product->get_catalog_visibility() === 'hidden') {
     59                    continue;
     60                }
    5461                $this->upsert_product($product, $table, $now);
    5562                $synced_ids[] = $product->get_id();
    5663            }
    5764
     65            $fetched += count($products);
    5866            $remaining -= count($products);
    5967            $page++;
    … …  
    6472        }
    6573
    66         // Chỉ dọn khi quét toàn bộ, nếu không sẽ xoá nhầm phần chưa duyệt tới.
    67         if ($full_sync && !empty($synced_ids)) {
     74        // Chỉ dọn khi quét toàn bộ, nếu không sẽ xoá nhầm phần chưa duyệt tới. Có sản phẩm mà tất cả đều ẩn
     75        // thì cũng dọn hết; WooCommerce không trả sản phẩm nào thì giữ nguyên (có thể chỉ là lỗi tạm thời).
     76        if ($full_sync && $fetched > 0) {
     77            $keep = $synced_ids ?: [0];
    6878            $wpdb->query($wpdb->prepare(
    69                 'DELETE FROM %i WHERE product_id NOT IN (' . implode(',', array_fill(0, count($synced_ids), '%d')) . ')',
    70                 array_merge([$table], $synced_ids)
     79                'DELETE FROM %i WHERE product_id NOT IN (' . implode(',', array_fill(0, count($keep), '%d')) . ')',
     80                array_merge([$table], $keep)
    7181            ));
    7282        }
    … …  
    127137
    128138        $price = $product->get_regular_price();
    129         $sale_price = $product->get_sale_price();
     139        // Giá khuyến mãi chỉ khi đang khuyến mãi (lịch giảm giá chưa tới thì chưa tính), và 0 là giá thật:
     140        // sản phẩm giảm còn 0 ₫ từng bị báo giá gốc 725.000 ₫ vì 0 bị coi là "không giảm" (ninhle.com 26/10).
     141        // NULL trong bảng cache = không khuyến mãi.
     142        $sale_price = $product->is_on_sale() ? $product->get_price() : '';
    130143        // Sản phẩm có biến thể (size, màu) không có giá ở sản phẩm cha. Lưu giá hiện hành thấp nhất của
    131144        // các biến thể; để trống thì bot báo "không thấy giá" và bộ lọc theo giá bỏ sót sản phẩm.
    … …  
    215228                $p['name'],
    216229                (!empty($p['price_is_minimum']) ? 'từ ' : '') . $this->format_price($p['final_price']),
    217                 ($p['sale_price'] && $p['sale_price'] != $p['price']) ? ' (gốc ' . $this->format_price($p['price']) . ')' : '',
     230                (isset($p['sale_price']) && $p['sale_price'] !== '' && (float) $p['sale_price'] != (float) $p['price']) ? ' (gốc ' . $this->format_price($p['price']) . ')' : '',
    218231                $this->describe_stock($p['stock_status'], $p['stock_quantity'] ?? null),
    219232                $p['categories'] ?: 'Chưa phân loại',
    … …  
    227240        $context .= "\nCần chi tiết sâu hơn (tính năng, FAQ, thuộc tính) thì gọi tool get_product_detail với ID ở trên.\n";
    228241
     242        $this->context_ids = array_map(fn($p) => (int) $p['id'], $products);
    229243        return $context;
     244    }
     245
     246    /** @var int[] ID sản phẩm của [PRODUCT CATALOG] vừa dựng (để ToolResultLedger::know). */
     247    private $context_ids = [];
     248
     249    /** ID sản phẩm đã đưa vào [PRODUCT CATALOG] ở lần gọi get_context_for_ai() gần nhất. */
     250    public function context_ids() {
     251        return $this->context_ids;
    230252    }
    231253
    … …  
    252274               AND (%s = '' OR search_text LIKE %s OR title LIKE %s OR sku LIKE %s)
    253275               AND (%s = '' OR categories LIKE %s)
    254                AND (%f <= 0 OR CAST(COALESCE(NULLIF(sale_price, 0), price) AS DECIMAL(12,2)) >= %f)
    255                AND (%f <= 0 OR CAST(COALESCE(NULLIF(sale_price, 0), price) AS DECIMAL(12,2)) <= %f)
     276               AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) >= %f)
     277               AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) <= %f)
    256278             ORDER BY relevance DESC, total_sales DESC, cache_synced_at DESC
    257279             LIMIT %d",
    … …  
    274296        ), ARRAY_A);
    275297
     298        // Cả cụm không khớp liền một chỗ (VD "thanh toán QR WooCommerce" với tên "... Thanh Toán QR Tĩnh Cho
     299        // WooCommerce", ninhle.com 25/10): tìm theo từng từ, xếp theo số từ khớp.
     300        if (!$results && $has_query) {
     301            $results = $this->search_by_words($query, $category_like, $budget_min, $budget_max, (int) $limit, $in_stock_only);
     302        }
     303
    276304        return array_map([$this, 'format_row'], $results ?: []);
     305    }
     306
     307    /** Từ quá phổ biến, không giúp phân biệt sản phẩm. */
     308    const STOP_WORDS = ['cho', 'của', 'và', 'có', 'không', 'là', 'với', 'các', 'những', 'nào', 'shop', 'the', 'for', 'and', 'with', 'of', 'to', 'in', 'a', 'an'];
     309
     310    /** Các từ đáng tìm của một câu: chữ thường, từ 2 ký tự, bỏ từ phổ biến, tối đa 8 từ. */
     311    public static function query_words($query) {
     312        $words = preg_split('/[^\p{L}\p{N}]+/u', mb_strtolower((string) $query, 'UTF-8'), -1, PREG_SPLIT_NO_EMPTY) ?: [];
     313        $words = array_filter($words, fn($w) => mb_strlen($w, 'UTF-8') >= 2 && !in_array($w, self::STOP_WORDS, true));
     314        return array_slice(array_values(array_unique($words)), 0, 8);
     315    }
     316
     317    /**
     318     * Tìm theo từng từ: mỗi từ khớp tên, SKU hoặc search_text được một điểm. Câu 1–2 từ cần khớp hết; câu dài
     319     * hơn được thiếu một từ (khách hay thêm chữ thừa). Cùng bộ lọc danh mục, giá, còn hàng như tìm theo cụm.
     320     */
     321    private function search_by_words($query, $category_like, $budget_min, $budget_max, $limit, $in_stock_only) {
     322        global $wpdb;
     323        $words = self::query_words($query);
     324        if (count($words) < 2) {
     325            return [];
     326        }
     327        $score = [];
     328        $args = [];
     329        foreach ($words as $word) {
     330            $like = '%' . $wpdb->esc_like($word) . '%';
     331            $score[] = '(CASE WHEN search_text LIKE %s OR title LIKE %s OR sku LIKE %s THEN 1 ELSE 0 END)';
     332            array_push($args, $like, $like, $like);
     333        }
     334        $need = count($words) <= 2 ? count($words) : count($words) - 1;
     335        $sql = 'SELECT * FROM (
     336                    SELECT product_id, title, sku, product_type, price, sale_price, stock_status, stock_quantity,
     337                           short_description, categories, permalink, featured_image, total_sales, cache_synced_at,
     338                           ' . implode(' + ', $score) . ' AS matched
     339                    FROM %i
     340                    WHERE (%d = 0 OR stock_status = \'instock\')
     341                      AND (%s = \'\' OR categories LIKE %s)
     342                      AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) >= %f)
     343                      AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) <= %f)
     344                ) AS candidates
     345                WHERE matched >= %d
     346                ORDER BY matched DESC, total_sales DESC, cache_synced_at DESC
     347                LIMIT %d';
     348        $args = array_merge($args, [
     349            $this->table(), $in_stock_only ? 1 : 0, $category_like, $category_like,
     350            $budget_min, $budget_min, $budget_max, $budget_max, $need, $limit,
     351        ]);
     352        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql chỉ ghép các mẫu %s cố định; mọi giá trị qua prepare().
     353        return $wpdb->get_results($wpdb->prepare($sql, $args), ARRAY_A) ?: [];
    277354    }
    278355
    … …  
    313390            'price'             => $row['price'],
    314391            'sale_price'        => $row['sale_price'],
    315             'final_price'       => ($row['sale_price'] > 0) ? $row['sale_price'] : $row['price'],
     392            // NULL = không khuyến mãi; "0.00" là giá khuyến mãi thật (miễn phí).
     393            'final_price'       => (isset($row['sale_price']) && $row['sale_price'] !== '') ? $row['sale_price'] : $row['price'],
    316394            // Có biến thể: price là giá thấp nhất; kèm khoảng giá thật để model không gán giá thấp nhất
    317395            // cho một size/màu cụ thể.
  • ninhle-ai-sales-assistant/tags/2.6.0/vite.config.ts

    r3761175 r3765552  
    44import { resolve } from 'path';
    55
    6 // Build hai đầu ra từ mã nguồn trong src/JS:
    7 //  - widget: khung chat ngoài trang (Preact)
    8 //  - admin : giao diện quản trị (JSX qua plugin React, alias sang Preact)
     6// Build ba đầu ra từ mã nguồn trong src/JS:
     7//  - widget    : khung chat ngoài trang (Preact)
     8//  - widget-pro: module giao diện gói Starter (2.6.0), widget tự tải cạnh bundle của nó. Chỉ có trong gói bán:
     9//                build.ps1 -Target wporg loại file này, mã nguồn src/JS/widget-pro và mục của nó trong manifest.
     10//  - admin     : giao diện quản trị (JSX qua plugin React, alias sang Preact)
    911// Chạy: npm ci && npm run build  →  assets/dist/
    1012// build.ps1 chép assets/dist/.vite/manifest.json ra assets/dist/manifest.json khi đóng gói.
    … …  
    1416    plugins: [
    1517      preact({
    16         include: /widget\/.*\.tsx?$/,
     18        include: /widget(-pro)?\/.*\.tsx?$/,
    1719      }),
    1820      react({
    … …  
    3032      manifest: true,
    3133      rollupOptions: {
     34        // Vite (bản build ứng dụng) mặc định bỏ export của entry; widget-pro là module widget import động nên
     35        // phải giữ export. Entry không export gì (widget, admin) không đổi.
     36        preserveEntrySignatures: 'exports-only',
    3237        input: {
    3338          widget: resolve(__dirname, 'src/JS/widget/index.tsx'),
    34           admin: resolve(__dirname, 'src/JS/admin/index.tsx')
     39          'widget-pro': resolve(__dirname, 'src/JS/widget-pro/index.tsx'),
     40          admin: resolve(__dirname, 'src/JS/admin/index.tsx'),
     41          // Tab Sales Insights (2.6.0): admin nạp bằng URL khi mở tab (insightsLoader.ts); React dùng chung qua chunk chung.
     42          'admin-insights': resolve(__dirname, 'src/JS/admin/insights/index.tsx')
    3543        },
    3644        output: {
  • ninhle-ai-sales-assistant/trunk/assets/dist/admin.bundle.js

    r3761175 r3765552  
    1 import{C as Q,l as P,k as be,S as z,F as ne,R as Ie,d as p,A as Y,q as re,u as e,h as j}from"./assets/hooks.module-C33f2O-M.js";function Ge(r,t){for(var s in t)r[s]=t[s];return r}function ge(r,t){for(var s in r)if(s!=="__source"&&!(s in t))return!0;for(var a in t)if(a!=="__source"&&r[a]!==t[a])return!0;return!1}function fe(r,t){this.props=r,this.context=t}(fe.prototype=new Q).isPureReactComponent=!0,fe.prototype.shouldComponentUpdate=function(r,t){return ge(this.props,r)||ge(this.state,t)};var ye=P.__b;P.__b=function(r){r.type&&r.type.__f&&r.ref&&(r.props.ref=r.ref,r.ref=null),ye&&ye(r)};var Je=P.__e;P.__e=function(r,t,s,a){if(r.then){for(var o,d=t;d=d.__;)if((o=d.__c)&&o.__c)return t.__e==null&&(t.__e=s.__e,t.__k=s.__k||[]),o.__c(r,t)}Je(r,t,s,a)};var ve=P.unmount;function Pe(r,t,s){return r&&(r.__c&&r.__c.__H&&(r.__c.__H.__.forEach(function(a){typeof a.__c=="function"&&a.__c()}),r.__c.__H=null),(r=Ge({},r)).__c!=null&&(r.__c.__P===s&&(r.__c.__P=t),r.__c.__e=!0,r.__c=null),r.__k=r.__k&&r.__k.map(function(a){return Pe(a,t,s)})),r}function Oe(r,t,s){return r&&s&&(r.__v=null,r.__k=r.__k&&r.__k.map(function(a){return Oe(a,t,s)}),r.__c&&r.__c.__P===t&&(r.__e&&s.appendChild(r.__e),r.__c.__e=!0,r.__c.__P=s)),r}function se(){this.__u=0,this.o=null,this.__b=null}function Re(r){var t=r.__&&r.__.__c;return t&&t.__a&&t.__a(r)}function te(){this.i=null,this.l=null}P.unmount=function(r){var t=r.__c;t&&(t.__z=!0),t&&t.__R&&t.__R(),t&&32&r.__u&&(r.type=null),ve&&ve(r)},(se.prototype=new Q).__c=function(r,t){var s=t.__c,a=this;a.o==null&&(a.o=[]),a.o.push(s);var o=Re(a.__v),d=!1,i=function(){d||a.__z||(d=!0,s.__R=null,o?o(b):b())};s.__R=i;var f=s.__P;s.__P=null;var b=function(){if(!--a.__u){if(a.state.__a){var _=a.state.__a;a.__v.__k[0]=Oe(_,_.__c.__P,_.__c.__O)}var E;for(a.setState({__a:a.__b=null});E=a.o.pop();)E.__P=f,E.forceUpdate()}};a.__u++||32&t.__u||a.setState({__a:a.__b=a.__v.__k[0]}),r.then(i,i)},se.prototype.componentWillUnmount=function(){this.o=[]},se.prototype.render=function(r,t){if(this.__b){if(this.__v.__k){var s=document.createElement("div"),a=this.__v.__k[0].__c;this.__v.__k[0]=Pe(this.__b,s,a.__O=a.__P)}this.__b=null}var o=t.__a&&be(z,null,r.fallback);return o&&(o.__u&=-33),[be(z,null,t.__a?null:r.children),o]};var we=function(r,t,s){if(++s[1]===s[0]&&r.l.delete(t),r.props.revealOrder&&(r.props.revealOrder[0]!=="t"||!r.l.size))for(s=r.i;s;){for(;s.length>3;)s.pop()();if(s[1]<s[0])break;r.i=s=s[2]}};(te.prototype=new Q).__a=function(r){var t=this,s=Re(t.__v),a=t.l.get(r);return a[0]++,function(o){var d=function(){t.props.revealOrder?(a.push(o),we(t,r,a)):o()};s?s(d):d()}},te.prototype.render=function(r){this.i=null,this.l=new Map;var t=ne(r.children);r.revealOrder&&r.revealOrder[0]==="b"&&t.reverse();for(var s=t.length;s--;)this.l.set(t[s],this.i=[1,0,this.i]);return r.children},te.prototype.componentDidUpdate=te.prototype.componentDidMount=function(){var r=this;this.l.forEach(function(t,s){we(r,s,t)})};var Ke=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.element")||60103,Ye=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,Ze=/^on(Ani|Tra|Tou|BeforeInp|Compo)/,Xe=/[A-Z0-9]/g,Qe=typeof document<"u",et=function(r){return(typeof Symbol<"u"&&typeof Symbol()=="symbol"?/fil|che|rad/:/fil|che|ra/).test(r)};function tt(r,t,s){return t.__k==null&&(t.textContent=""),Ie(r,t),typeof s=="function"&&s(),r?r.__c:null}Q.prototype.isReactComponent=!0,["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(r){Object.defineProperty(Q.prototype,r,{configurable:!0,get:function(){return this["UNSAFE_"+r]},set:function(t){Object.defineProperty(this,r,{configurable:!0,writable:!0,value:t})}})});var ke=P.event;P.event=function(r){return ke&&(r=ke(r)),r.persist=function(){},r.isPropagationStopped=function(){return this.cancelBubble},r.isDefaultPrevented=function(){return this.defaultPrevented},r.nativeEvent=r};var rt={configurable:!0,get:function(){return this.class}},Ne=P.vnode;P.vnode=function(r){typeof r.type=="string"&&function(t){var s=t.props,a=t.type,o={},d=a.indexOf("-")==-1;for(var i in s){var f=s[i];if(!(i==="value"&&"defaultValue"in s&&f==null||Qe&&i==="children"&&a==="noscript"||i==="class"||i==="className")){var b=i.toLowerCase();i==="defaultValue"&&"value"in s&&s.value==null?i="value":i==="download"&&f===!0?f="":b==="translate"&&f==="no"?f=!1:b[0]==="o"&&b[1]==="n"?b==="ondoubleclick"?i="ondblclick":b!=="onchange"||a!=="input"&&a!=="textarea"||et(s.type)?b==="onfocus"?i="onfocusin":b==="onblur"?i="onfocusout":Ze.test(i)&&(i=b):b=i="oninput":d&&Ye.test(i)?i=i.replace(Xe,"-$&").toLowerCase():f===null&&(f=void 0),b==="oninput"&&o[i=b]&&(i="oninputCapture"),o[i]=f}}a=="select"&&(o.multiple&&Array.isArray(o.value)&&(o.value=ne(s.children).forEach(function(_){_.props.selected=o.value.indexOf(_.props.value)!=-1})),o.defaultValue!=null&&(o.value=ne(s.children).forEach(function(_){_.props.selected=o.multiple?o.defaultValue.indexOf(_.props.value)!=-1:o.defaultValue==_.props.value}))),s.class&&!s.className?(o.class=s.class,Object.defineProperty(o,"className",rt)):s.className&&(o.class=o.className=s.className),t.props=o}(r),r.$$typeof=Ke,Ne&&Ne(r)};var xe=P.__r;P.__r=function(r){xe&&xe(r),r.__c};var Ce=P.diffed;P.diffed=function(r){Ce&&Ce(r);var t=r.props,s=r.__e;s!=null&&r.type==="textarea"&&"value"in t&&t.value!==s.value&&(s.value=t.value==null?"":t.value)};function st(r){return!!r.__k&&(Ie(null,r),!0)}function Be(r){return{render:function(t){tt(t,r)},unmount:function(){st(r)}}}var Ee;const x=((Ee=window.SMBCommerceAdmin)==null?void 0:Ee.restUrl)||"/wp-json/smb-commerce/v1";async function R(r){const t=await r.text();return t?JSON.parse(t):null}const N={getHeaders(){var t;return{"Content-Type":"application/json",Accept:"application/json","X-WP-Nonce":((t=window.SMBCommerceAdmin)==null?void 0:t.nonce)||""}},async get(r){const t=await fetch(`${x}${r}`,{method:"GET",headers:this.getHeaders()});if(!t.ok)throw new Error(`GET ${r} failed`);return await R(t)},async post(r,t={}){const s=await fetch(`${x}${r}`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify(t)});if(!s.ok)throw new Error(`POST ${r} failed`);return await R(s)},async delete(r){const t=await fetch(`${x}${r}`,{method:"DELETE",headers:this.getHeaders()});if(!t.ok)throw new Error(`DELETE ${r} failed`);return await R(t)},async getConversations(r,t,s=1){const a=new URL(`${x}/admin/conversations`,window.location.origin);r&&a.searchParams.append("status",r),t!==void 0&&a.searchParams.append("assigned_agent_id",t.toString()),a.searchParams.append("page",s.toString());const o=await fetch(a.toString(),{method:"GET",headers:this.getHeaders()});if(!o.ok)throw new Error("Failed to fetch conversations");return await R(o)},async getMessages(r,t=null){const s=new URL(`${x}/admin/conversations/${r}/messages`,window.location.origin);t&&s.searchParams.append("last_message_id",t.toString());const a=await fetch(s.toString(),{method:"GET",headers:this.getHeaders()});if(!a.ok)throw new Error("Failed to fetch messages");return await R(a)},async sendMessage(r,t,s){const a=await fetch(`${x}/admin/conversations/${r}/message`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify({content:t,client_message_id:s})});if(!a.ok)throw new Error("Failed to send message");return await R(a)},async takeover(r){const t=await fetch(`${x}/admin/conversations/${r}/takeover`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to takeover conversation");return await R(t)},async release(r){const t=await fetch(`${x}/admin/conversations/${r}/release`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to release conversation");return await R(t)},async close(r){const t=await fetch(`${x}/admin/conversations/${r}/close`,{method:"POST",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to close conversation");return await R(t)},async deleteConversation(r){const t=await fetch(`${x}/admin/conversations/${r}`,{method:"DELETE",headers:this.getHeaders()});if(!t.ok)throw new Error("Failed to delete conversation");return await R(t)},async getLeads(r=1,t=20){const s=new URL(`${x}/admin/leads`,window.location.origin);s.searchParams.append("page",r.toString()),s.searchParams.append("per_page",t.toString());const a=await fetch(s.toString(),{method:"GET",headers:this.getHeaders()});if(!a.ok)throw new Error("Failed to fetch leads");return await R(a)},async bulkDeleteConversations(r){const t=await fetch(`${x}/admin/conversations/bulk-delete`,{method:"POST",headers:this.getHeaders(),body:JSON.stringify({ids:r})});if(!t.ok)throw new Error("Bulk delete failed");return await R(t)}};function ce(){const[r,t]=p([]),s=Y(1),a=re(d=>t(i=>i.filter(f=>f.id!==d)),[]),o=re((d,i)=>{const f=s.current++;t(b=>[...b.filter(_=>_.text!==i),{id:f,type:d,text:i}]),(d==="success"||d==="info")&&window.setTimeout(()=>a(f),6e3)},[a]);return{notices:r,push:o,dismiss:a}}function ie({notices:r,onDismiss:t}){return r.length===0?null:e("div",{className:"smb-notices",children:r.map(s=>e("div",{className:`notice notice-${s.type} is-dismissible smb-inline-notice`,role:s.type==="error"?"alert":"status",children:[e("p",{children:s.text}),e("button",{type:"button",className:"notice-dismiss",onClick:()=>t(s.id),children:e("span",{className:"screen-reader-text",children:"Dismiss this notice."})})]},s.id))})}function le(){const[r,t]=p(null),s=re((o,d,i)=>{t({message:o,confirmLabel:d,onConfirm:i})},[]),a=re(()=>t(null),[]);return{request:r,ask:s,cancel:a}}function me({request:r,onCancel:t}){const s=Y(null),[a,o]=p(!1);if(j(()=>{var i;r&&((i=s.current)==null||i.focus())},[r]),!r)return null;const d=async()=>{o(!0);try{await r.onConfirm()}finally{o(!1),t()}};return e("div",{className:"smb-confirm",role:"alertdialog","aria-modal":"false","aria-labelledby":"smb-confirm-text",onKeyDown:i=>{i.key==="Escape"&&t()},children:[e("p",{id:"smb-confirm-text",children:r.message}),e("div",{className:"smb-confirm-actions",children:[e("button",{type:"button",className:"button button-primary smb-button-danger",onClick:d,disabled:a,children:r.confirmLabel}),e("button",{type:"button",className:"button",ref:s,onClick:t,disabled:a,children:"Cancel"})]})]})}const nt={bot_active:"AI assistant",human_active:"Waiting for your team",awaiting_agent:"Waiting for your team",agent_active:"Team member replying",closed:"Closed"},at={webchat:"Website chat"},Se=r=>nt[r]||r,ot=r=>r.sender_type==="user"?"Customer":r.sender_type==="agent"?r.first_name?`${r.first_name} (team)`:"Team member":r.sender_type==="system"?"Assistant":r.sender_type;function ct(){const{notices:r,push:t,dismiss:s}=ce(),a=le(),[o,d]=p([]),[i,f]=p(null),[b,_]=p([]),[E,y]=p(""),[w,$]=p(new Set),[B,I]=p(!1),D=Y(null),C=Y(5e3),u=Y(null),h=Y(null);j(()=>{S()},[]);const S=async()=>{try{const m=await N.getConversations();d(m.items||[])}catch(m){console.error(m)}},H=async m=>{var k;f(m);try{const T=await N.getMessages(m.id);_(T.messages||[]),h.current=((k=T.messages)==null?void 0:k.length)>0?T.messages[T.messages.length-1].id:null,U()}catch(T){console.error(T)}},U=()=>setTimeout(()=>{var m;return(m=D.current)==null?void 0:m.scrollIntoView({behavior:"smooth"})},100),O=async()=>{var m;if(!i||document.visibilityState!=="visible"){u.current=window.setTimeout(O,C.current);return}try{const k=await N.getMessages(i.id,h.current);((m=k.messages)==null?void 0:m.length)>0&&(_(T=>{const v=new Set(k.messages.map(M=>M.id));return[...T.filter(M=>!v.has(M.id)),...k.messages]}),h.current=k.messages[k.messages.length-1].id,U(),C.current=5e3)}catch{C.current=Math.min(C.current*2,6e4)}u.current=window.setTimeout(O,C.current)};j(()=>(i&&(u.current=window.setTimeout(O,C.current)),()=>{u.current&&clearTimeout(u.current)}),[i]);const V=async()=>{if(i)try{await N.takeover(i.id),f({...i,status:"agent_active"}),S()}catch{t("error","Takeover failed. Another agent may already have this conversation.")}},A=async()=>{if(i)try{await N.release(i.id),f({...i,status:"bot_active"}),S()}catch{t("error","Could not hand the conversation back to the bot.")}},q=(m,k)=>{k.stopPropagation(),a.ask(`Permanently delete conversation #${m}? This cannot be undone.`,"Delete",async()=>{try{await N.deleteConversation(m),(i==null?void 0:i.id)===m&&(f(null),_([])),$(T=>{const v=new Set(T);return v.delete(m),v}),S(),t("success",`Conversation #${m} deleted.`)}catch{t("error","Could not delete the conversation.")}})},K=(m,k)=>{k.stopPropagation(),$(T=>{const v=new Set(T);return v.has(m)?v.delete(m):v.add(m),v})},Z=()=>{w.size===o.length&&o.length>0?$(new Set):$(new Set(o.map(m=>m.id)))},g=()=>{if(w.size===0)return;const m=w.size;a.ask(`Permanently delete ${m} selected conversation(s)? This cannot be undone.`,`Delete ${m}`,async()=>{I(!0);try{const k=await N.bulkDeleteConversations(Array.from(w));i&&w.has(i.id)&&(f(null),_([])),$(new Set),S(),k.failed>0?t("warning",`${k.failed} conversation(s) could not be deleted.`):t("success",`${m} conversation(s) deleted.`)}catch{t("error","Bulk delete failed.")}finally{I(!1)}})},G=async()=>{if(!E.trim()||!i)return;const m=E.trim();y("");const k=crypto.randomUUID(),T={id:-Date.now(),sender_type:"agent",content:m,created_at:new Date().toISOString()};_(v=>[...v,T]),U();try{const v=await N.sendMessage(i.id,m,k);_(M=>M.map(ee=>ee.id===T.id?v:ee)),h.current=v.id}catch{t("error","Could not send the message. Your text is back in the reply box."),_(v=>v.filter(M=>M.id!==T.id)),y(m)}},W={bot_active:"#10b981",human_active:"#f59e0b",agent_active:"#3b82f6",closed:"#9ca3af"};return e(z,{children:[e(ie,{notices:r,onDismiss:s}),e(me,{request:a.request,onCancel:a.cancel}),e("div",{className:"smb-commerce-inbox-container",children:[e("div",{className:"smb-commerce-sidebar",children:[e("div",{style:{padding:"12px 15px",borderBottom:"1px solid #ccd0d4",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("h2",{style:{margin:0,fontSize:"15px",fontWeight:700},children:["Conversations (",o.length,")"]}),e("button",{className:"button",style:{fontSize:"12px",padding:"3px 8px"},onClick:Z,children:w.size===o.length&&o.length>0?"Deselect all":"Select all"})]}),w.size>0&&e("div",{style:{padding:"8px 12px",background:"#fff3cd",borderBottom:"1px solid #f0c36d",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("span",{style:{fontSize:"13px",fontWeight:600,color:"#856404"},children:["Selected: ",w.size]}),e("button",{style:{background:"#d63638",color:"white",border:"none",fontSize:"12px",padding:"4px 12px",borderRadius:"4px",cursor:"pointer",fontWeight:600},onClick:g,disabled:B,children:B?"Deleting…":"Delete "+w.size})]}),e("ul",{className:"smb-commerce-conversation-list",style:{listStyle:"none",margin:0,padding:0},children:o.map(m=>e("li",{className:"smb-commerce-conversation-item "+((i==null?void 0:i.id)===m.id?"active":""),onClick:()=>H(m),style:{display:"flex",alignItems:"center",gap:"8px",padding:"10px 12px",background:w.has(m.id)?"#eff6ff":void 0},children:[e("input",{type:"checkbox",checked:w.has(m.id),onClick:k=>K(m.id,k),onChange:()=>{},style:{width:"15px",height:"15px",flexShrink:0,cursor:"pointer"}}),e("div",{style:{flex:1,minWidth:0},children:[e("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"4px"},children:[e("span",{style:{fontWeight:600,fontSize:"13px"},children:["Conversation #",m.id]}),e("span",{style:{fontSize:"10px",fontWeight:600,color:"white",background:W[m.status]||"#9ca3af",padding:"1px 6px",borderRadius:"10px",whiteSpace:"nowrap"},children:Se(m.status)})]}),e("div",{style:{fontSize:"11px",color:"#666",marginTop:"2px"},children:at[m.channel]||m.channel})]}),e("button",{style:{color:"#d63638",padding:"2px",background:"none",border:"none",cursor:"pointer",flexShrink:0},onClick:k=>q(m.id,k),title:"Delete conversation","aria-label":`Delete conversation #${m.id}`,children:e("span",{className:"dashicons dashicons-trash",style:{fontSize:"16px"}})})]},m.id))})]}),e("div",{className:"smb-commerce-main",children:i?e(z,{children:[e("div",{className:"smb-commerce-main-header",children:[e("h3",{children:["Conversation #",i.id," — ",e("span",{style:{color:W[i.status]},children:Se(i.status)})]}),e("div",{className:"actions",children:[i.status!=="agent_active"&&e("button",{onClick:V,className:"button button-primary",children:"Take over"}),i.status==="agent_active"&&e("button",{onClick:A,className:"button button-secondary",children:"Hand back to bot"})]})]}),e("div",{className:"smb-commerce-chat-history",children:[b.map(m=>e("div",{className:"smb-commerce-admin-msg "+m.sender_type,children:[e("div",{className:"msg-sender",children:ot(m)}),e("div",{className:"msg-content",children:m.content})]},m.id)),e("div",{ref:D})]}),e("div",{className:"smb-commerce-main-input",children:[e("input",{type:"text",value:E,onInput:m=>y(m.target.value),onKeyPress:m=>m.key==="Enter"&&G(),placeholder:"Type your reply…",disabled:i.status!=="agent_active"&&i.status!=="awaiting_agent"}),e("button",{className:"button button-primary",onClick:G,disabled:i.status!=="agent_active"&&i.status!=="awaiting_agent",children:"Send"})]})]}):e("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",color:"#666"},children:"Select a conversation to get started"})})]})]})}const it={"message.created":"Visitor sends a message","conversation.created":"Visitor opens the chat"},lt={content:"Message text",time:"Site hour"},Te={soft:"normal priority",hard:"high priority",emergency:"urgent"},De=r=>{if(typeof r!="string")return r||{};try{return JSON.parse(r)||{}}catch{return{}}},mt=r=>{const t=De(r),s=t.conditions;if(!Array.isArray(s)||s.length===0)return"Always";const a=o=>String(o??"").split(",").map(d=>d.trim()).filter(Boolean).map(d=>`"${d}"`).join(" or ");return s.map(o=>{if(o.type==="message_contains")return`Message text contains "${o.value}"`;if(o.type==="conversation.has_tag")return`Conversation has the tag "${o.value}"`;const d=lt[o.field]||o.field;return o.operator==="contains"?`${d} contains ${a(o.value)}`:o.operator==="equals"?`${d} is exactly "${o.value}"`:o.operator==="after"?`${d} is ${o.value}:00 or later`:JSON.stringify(o)}).join(t.type==="or"?" or ":" and ")},dt=r=>{const t=De(r).actions;return!Array.isArray(t)||t.length===0?JSON.stringify(r):t.map(s=>s.type==="handoff"?`Hand off to a person (${Te[String(s.value||"").toLowerCase()]||Te.soft})`:s.type==="send_message"?`Reply: "${s.value}"`:s.type==="tag_conversation"?`Add the tag "${s.value}"`:JSON.stringify(s)).join(", then ")},ht=()=>{const{notices:r,push:t,dismiss:s}=ce(),a=le(),[o,d]=p(""),[i,f]=p([]),[b,_]=p(!0),[E,y]=p(!1),[w,$]=p(""),[B,I]=p("message.created"),[D,C]=p("content"),[u,h]=p("contains"),[S,H]=p(""),[U,O]=p("handoff"),[V,A]=p("Soft"),q=async()=>{_(!0);try{const g=await N.get("/admin/rules");g.items&&f(g.items)}catch(g){console.error("Failed to load rules",g)}_(!1)};j(()=>{q()},[]);const K=(g,G)=>{a.ask(`Delete the rule "${G}"?`,"Delete rule",async()=>{try{await N.delete(`/admin/rules/${g}`),q(),t("success","Rule deleted.")}catch{t("error","Could not delete the rule.")}})},Z=async()=>{if(!w.trim()){d("Please enter a rule name.");return}d("");const g={name:w,trigger_event:B,conditions:{type:"and",conditions:[{type:"simple",field:D,operator:u,value:S}]},actions:{actions:[{type:U,value:V}]},priority:10,is_active:1};try{await N.post("/admin/rules",g),y(!1),$(""),H(""),q(),t("success","Rule saved and enabled.")}catch{t("error","Could not save the rule.")}};return b?e("div",{role:"status",children:"Loading…"}):e("div",{className:"smb-commerce-rules",children:[e(ie,{notices:r,onDismiss:s}),e(me,{request:a.request,onCancel:a.cancel}),e("h2",{children:"Automation rules"}),e("p",{children:"Automatically run an action when a visitor sends a message or opens the chat, based on conditions you define."}),E?e("div",{className:"rule-form",style:{background:"#fff",padding:"20px",border:"1px solid #ccd0d4",borderRadius:"4px",maxWidth:"800px"},children:[e("h3",{style:{marginTop:0},children:"Create an automation rule"}),e("div",{style:{marginBottom:15},children:[e("label",{htmlFor:"smb-rule-name",style:{fontWeight:600},children:"Rule name:"}),e("br",{}),e("input",{id:"smb-rule-name",type:"text",className:"regular-text",style:{width:"100%",maxWidth:"400px"},value:w,onChange:g=>{$(g.target.value),o&&d("")},"aria-invalid":o?!0:void 0,"aria-describedby":o?"smb-rule-name-error":void 0,placeholder:"e.g. Hand off when a visitor complains"}),o&&e("p",{id:"smb-rule-name-error",className:"smb-field-error",role:"alert",children:o})]}),e("div",{style:{marginBottom:15},children:[e("label",{style:{fontWeight:600},children:"1. When this happens (trigger):"}),e("br",{}),e("select",{value:B,onChange:g=>I(g.target.value),style:{width:"100%",maxWidth:"400px"},children:[e("option",{value:"message.created",children:"Visitor sends a new message"}),e("option",{value:"conversation.created",children:"Visitor opens the chat for the first time"})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #2271b1"},children:[e("label",{style:{fontWeight:600},children:"2. AND these conditions match (if):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:D,onChange:g=>C(g.target.value),children:[e("option",{value:"content",children:"Message text"}),e("option",{value:"time",children:"Current time (site hour, 0–23)"})]}),e("select",{value:u,onChange:g=>h(g.target.value),children:[e("option",{value:"contains",children:"Contains keyword"}),e("option",{value:"equals",children:"Exactly matches"}),e("option",{value:"after",children:"After (hour)"})]}),e("input",{type:"text",value:S,onChange:g=>H(g.target.value),placeholder:"e.g. refund, quote..."})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #00a32a"},children:[e("label",{style:{fontWeight:600},children:"3. THEN do this (then):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:U,onChange:g=>O(g.target.value),children:[e("option",{value:"handoff",children:"Hand off to a human agent"}),e("option",{value:"send_message",children:"Send an automatic message"}),e("option",{value:"tag_conversation",children:"Add a tag"})]}),e("input",{type:"text",value:V,onChange:g=>A(g.target.value),placeholder:"e.g. Soft (or the message text)",style:{flex:1}})]})]}),e("div",{style:{marginTop:20,paddingTop:"15px",borderTop:"1px solid #ddd"},children:[e("button",{onClick:Z,className:"button button-primary",children:"Save and enable"}),e("button",{onClick:()=>y(!1),className:"button",style:{marginLeft:10},children:"Cancel"})]})]}):e(z,{children:[e("button",{onClick:()=>y(!0),className:"button button-primary",style:{marginBottom:15},children:"+ New rule"}),e("table",{className:"wp-list-table widefat fixed striped",children:[e("thead",{children:e("tr",{children:[e("th",{children:"Rule name"}),e("th",{children:"When"}),e("th",{children:"If"}),e("th",{children:"Then"}),e("th",{children:"Status"}),e("th",{children:"Actions"})]})}),e("tbody",{children:[i.map(g=>e("tr",{children:[e("td",{children:e("strong",{children:g.name})}),e("td",{children:it[g.trigger_event]||g.trigger_event}),e("td",{children:mt(g.conditions)}),e("td",{children:dt(g.actions)}),e("td",{children:g.is_active?e("span",{style:{color:"green"},children:"Active"}):e("span",{style:{color:"gray"},children:"Disabled"})}),e("td",{children:e("button",{type:"button",onClick:()=>K(g.id,g.name),className:"button-link button-link-delete",children:"Delete"})})]},g.id)),i.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No rules created yet."})})]})]})]})]})},ut=()=>{const[r,t]=p([]),[s,a]=p(!0),o=async()=>{a(!0);try{const i=await N.getLeads();i.items&&t(i.items)}catch(i){console.error("Failed to load leads",i)}a(!1)};return j(()=>{o()},[]),s?e("div",{children:"Loading leads..."}):e("div",{className:"smb-commerce-leads",children:[e("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e("h2",{children:"Leads"}),e("button",{className:"button button-primary",onClick:()=>{var f,b;const i=`${((f=window.SMBCommerceAdmin)==null?void 0:f.restUrl)||"/wp-json/smb-commerce/v1"}/admin/leads/export?_wpnonce=${((b=window.SMBCommerceAdmin)==null?void 0:b.nonce)||""}`;window.location.href=i},children:[e("span",{className:"dashicons dashicons-download",style:{marginTop:"4px"}})," Export CSV"]})]}),e("table",{className:"wp-list-table widefat fixed striped",style:{marginTop:"10px"},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Name"}),e("th",{children:"Email"}),e("th",{children:"Phone"}),e("th",{children:"Score"}),e("th",{children:"Status"}),e("th",{children:"Created At"})]})}),e("tbody",{children:[r.map(i=>e("tr",{children:[e("td",{children:i.name||"-"}),e("td",{children:i.email||"-"}),e("td",{children:i.phone||"-"}),e("td",{children:i.score}),e("td",{children:i.status}),e("td",{children:new Date(i.created_at).toLocaleString()})]},i.id)),r.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No leads found."})})]})]})]})};function pt(){var ue,pe,_e;const r=le(),[t,s]=p({smbcommerce_brand_name:"Shop Assistant",smbcommerce_primary_color:"#01696f",smbcommerce_logo_url:"",smbcommerce_font_family:"system",smbcommerce_runtime_mode:"smart_hybrid",smbcommerce_ai_provider:"openai",smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:"",smbcommerce_telegram_chat_id:"",smbcommerce_sales_playbook:"",smbcommerce_business_info:"",smbcommerce_enable_adaptive_playbook:"1",smbcommerce_meta_prompt_template:"",smbcommerce_enable_prompt_creator:"1",smbcommerce_prompt_creator_template:"",smbcommerce_max_upload_size:"5",smbcommerce_handoff_webhook:"",smbcommerce_system_guardrails_prompt:"",smbcommerce_media_retention_days:"30",smbcommerce_widget_slogan:"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:"",smbcommerce_hotline:"",smbcommerce_zalo_link:"",smbcommerce_messenger_link:"",smbcommerce_whatsapp:"",smbcommerce_telegram_link:"",smbcommerce_ai_notice_enabled:"1",smbcommerce_ai_notice_text:"",smbcommerce_hide_on_checkout:"0",smbcommerce_voice_input_enabled:"0",smbcommerce_gemini_model:"",smbcommerce_anthropic_model:"",smbcommerce_openai_model:"",smbcommerce_refund_policy:"",smbcommerce_warranty_policy:"",smbcommerce_delivery_policy:"",smbcommerce_privacy_policy:"",smbcommerce_enable_link_reading:"0",smbcommerce_budget_daily_usd:"0",smbcommerce_budget_monthly_usd:"0",smbcommerce_enable_provider_fallback:"1",smbcommerce_block_ungrounded_prices:"0",smbcommerce_enable_response_cache:"0",smbcommerce_cache_ttl_hours:"24",smbcommerce_webhook_url:"",smbcommerce_webhook_secret:""}),[a,o]=p({openai:!1,anthropic:!1,gemini:!1,telegram:!1}),[d,i]=p(!1),[f,b]=p(""),[_,E]=p(null),[y,w]=p(!1),[$,B]=p(!1),[I,D]=p(null),[C,u]=p(null),[h,S]=p(null),[H,U]=p(30),[O,V]=p(null),[A,q]=p(null),[K,Z]=p({}),[g,G]=p(!0),[W,m]=p(null),[k,T]=p(null),v=n=>!K[n]||K[n].allowed;j(()=>{de(),he()},[]),j(()=>{M(H)},[H]);const M=async n=>{try{const c=await fetch(`${x}/admin/analytics?days=${n}`,{headers:N.getHeaders()});S(await c.json())}catch(c){console.error("Analytics load failed",c)}},ee=async()=>{V({pending:!0});try{const n=await fetch(`${x}/admin/webhook/test`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})});V(await n.json())}catch(n){V({message:n.message})}},Me=async()=>{try{await fetch(`${x}/admin/cache/flush`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})}),M(H),b("Cache cleared."),setTimeout(()=>b(""),3e3)}catch{b("Could not clear the cache.")}},J=n=>"$"+(Number(n)||0).toFixed(n<1?4:2);j(()=>{const n=t.smbcommerce_font_family;if(!n||n==="system")return;const c=`smb-commerce-font-${n.replace(/\s+/g,"-")}`;if(!document.getElementById(c)){const L=document.createElement("link");L.id=c,L.rel="stylesheet",L.href=`https://fonts.googleapis.com/css2?family=${n.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(L)}},[t.smbcommerce_font_family]);const de=async()=>{try{const c=await(await fetch(`${x}/admin/settings`,{headers:N.getHeaders()})).json();c.settings&&(s(L=>({...L,smbcommerce_brand_name:c.settings.smbcommerce_brand_name||"",smbcommerce_primary_color:c.settings.smbcommerce_primary_color||"",smbcommerce_logo_url:c.settings.smbcommerce_logo_url||"",smbcommerce_font_family:c.settings.smbcommerce_font_family||"system",smbcommerce_runtime_mode:c.settings.smbcommerce_runtime_mode||"smart_hybrid",smbcommerce_ai_provider:c.settings.smbcommerce_ai_provider||"openai",smbcommerce_telegram_chat_id:c.settings.smbcommerce_telegram_chat_id||"",smbcommerce_sales_playbook:c.settings.smbcommerce_sales_playbook||"",smbcommerce_business_info:c.settings.smbcommerce_business_info||"",smbcommerce_enable_adaptive_playbook:c.settings.smbcommerce_enable_adaptive_playbook||"1",smbcommerce_meta_prompt_template:c.settings.smbcommerce_meta_prompt_template||"",smbcommerce_enable_prompt_creator:c.settings.smbcommerce_enable_prompt_creator||"1",smbcommerce_prompt_creator_template:c.settings.smbcommerce_prompt_creator_template||"",smbcommerce_max_upload_size:c.settings.smbcommerce_max_upload_size||"5",smbcommerce_handoff_webhook:c.settings.smbcommerce_handoff_webhook||"",smbcommerce_system_guardrails_prompt:c.settings.smbcommerce_system_guardrails_prompt||"",smbcommerce_media_retention_days:c.settings.smbcommerce_media_retention_days||"30",smbcommerce_widget_slogan:c.settings.smbcommerce_widget_slogan||"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:c.settings.smbcommerce_suggestion_chips||"",smbcommerce_hotline:c.settings.smbcommerce_hotline||"",smbcommerce_zalo_link:c.settings.smbcommerce_zalo_link||"",smbcommerce_messenger_link:c.settings.smbcommerce_messenger_link||"",smbcommerce_whatsapp:c.settings.smbcommerce_whatsapp||"",smbcommerce_telegram_link:c.settings.smbcommerce_telegram_link||"",smbcommerce_ai_notice_enabled:c.settings.smbcommerce_ai_notice_enabled??"1",smbcommerce_ai_notice_text:c.settings.smbcommerce_ai_notice_text||"",smbcommerce_hide_on_checkout:c.settings.smbcommerce_hide_on_checkout??"0",smbcommerce_voice_input_enabled:c.settings.smbcommerce_voice_input_enabled??"0",smbcommerce_gemini_model:c.settings.smbcommerce_gemini_model||"",smbcommerce_anthropic_model:c.settings.smbcommerce_anthropic_model||"",smbcommerce_openai_model:c.settings.smbcommerce_openai_model||"",smbcommerce_refund_policy:c.settings.smbcommerce_refund_policy||"",smbcommerce_warranty_policy:c.settings.smbcommerce_warranty_policy||"",smbcommerce_delivery_policy:c.settings.smbcommerce_delivery_policy||"",smbcommerce_privacy_policy:c.settings.smbcommerce_privacy_policy||"",smbcommerce_enable_link_reading:c.settings.smbcommerce_enable_link_reading||"0",smbcommerce_budget_daily_usd:c.settings.smbcommerce_budget_daily_usd||"0",smbcommerce_budget_monthly_usd:c.settings.smbcommerce_budget_monthly_usd||"0",smbcommerce_enable_provider_fallback:c.settings.smbcommerce_enable_provider_fallback||"1",smbcommerce_block_ungrounded_prices:c.settings.smbcommerce_block_ungrounded_prices||"0",smbcommerce_enable_response_cache:c.settings.smbcommerce_enable_response_cache||"0",smbcommerce_cache_ttl_hours:c.settings.smbcommerce_cache_ttl_hours||"24",smbcommerce_webhook_url:c.settings.smbcommerce_webhook_url||"",smbcommerce_webhook_secret:""})),q(c.settings.smbcommerce_license||null),Z(c.settings.smbcommerce_features||{}),G(c.settings.smbcommerce_premium_build!==!1),m(c.settings.smbcommerce_model_report||null),T(c.settings.smbcommerce_business_info_is_sample?c.settings.smbcommerce_business_info:null),o({openai:c.settings.smbcommerce_openai_key_exists,anthropic:c.settings.smbcommerce_anthropic_key_exists,gemini:c.settings.smbcommerce_gemini_key_exists,telegram:c.settings.smbcommerce_telegram_bot_token_exists}))}catch(n){console.error("Failed to load settings",n)}},Fe=async n=>{n.preventDefault(),i(!0),b("");try{const c={smbcommerce_brand_name:t.smbcommerce_brand_name,smbcommerce_primary_color:t.smbcommerce_primary_color,smbcommerce_logo_url:t.smbcommerce_logo_url,smbcommerce_font_family:t.smbcommerce_font_family,smbcommerce_runtime_mode:t.smbcommerce_runtime_mode,smbcommerce_ai_provider:t.smbcommerce_ai_provider,smbcommerce_telegram_chat_id:t.smbcommerce_telegram_chat_id,smbcommerce_sales_playbook:t.smbcommerce_sales_playbook,smbcommerce_business_info:t.smbcommerce_business_info,smbcommerce_enable_adaptive_playbook:t.smbcommerce_enable_adaptive_playbook,smbcommerce_meta_prompt_template:t.smbcommerce_meta_prompt_template,smbcommerce_enable_prompt_creator:t.smbcommerce_enable_prompt_creator,smbcommerce_prompt_creator_template:t.smbcommerce_prompt_creator_template,smbcommerce_max_upload_size:t.smbcommerce_max_upload_size,smbcommerce_handoff_webhook:t.smbcommerce_handoff_webhook,smbcommerce_system_guardrails_prompt:t.smbcommerce_system_guardrails_prompt,smbcommerce_media_retention_days:t.smbcommerce_media_retention_days,smbcommerce_widget_slogan:t.smbcommerce_widget_slogan,smbcommerce_suggestion_chips:t.smbcommerce_suggestion_chips,smbcommerce_hotline:t.smbcommerce_hotline,smbcommerce_zalo_link:t.smbcommerce_zalo_link,smbcommerce_messenger_link:t.smbcommerce_messenger_link,smbcommerce_whatsapp:t.smbcommerce_whatsapp,smbcommerce_telegram_link:t.smbcommerce_telegram_link,smbcommerce_ai_notice_enabled:t.smbcommerce_ai_notice_enabled,smbcommerce_ai_notice_text:t.smbcommerce_ai_notice_text,smbcommerce_hide_on_checkout:t.smbcommerce_hide_on_checkout,smbcommerce_voice_input_enabled:t.smbcommerce_voice_input_enabled,smbcommerce_gemini_model:t.smbcommerce_gemini_model,smbcommerce_anthropic_model:t.smbcommerce_anthropic_model,smbcommerce_openai_model:t.smbcommerce_openai_model,smbcommerce_refund_policy:t.smbcommerce_refund_policy,smbcommerce_warranty_policy:t.smbcommerce_warranty_policy,smbcommerce_delivery_policy:t.smbcommerce_delivery_policy,smbcommerce_privacy_policy:t.smbcommerce_privacy_policy,smbcommerce_enable_link_reading:t.smbcommerce_enable_link_reading,smbcommerce_budget_daily_usd:t.smbcommerce_budget_daily_usd,smbcommerce_budget_monthly_usd:t.smbcommerce_budget_monthly_usd,smbcommerce_enable_provider_fallback:t.smbcommerce_enable_provider_fallback,smbcommerce_block_ungrounded_prices:t.smbcommerce_block_ungrounded_prices,smbcommerce_enable_response_cache:t.smbcommerce_enable_response_cache,smbcommerce_cache_ttl_hours:t.smbcommerce_cache_ttl_hours,smbcommerce_webhook_url:t.smbcommerce_webhook_url};t.smbcommerce_webhook_secret&&(c.smbcommerce_webhook_secret=t.smbcommerce_webhook_secret),t.smbcommerce_openai_key&&(c.smbcommerce_openai_key=t.smbcommerce_openai_key),t.smbcommerce_anthropic_key&&(c.smbcommerce_anthropic_key=t.smbcommerce_anthropic_key),t.smbcommerce_gemini_key&&(c.smbcommerce_gemini_key=t.smbcommerce_gemini_key),t.smbcommerce_telegram_bot_token&&(c.smbcommerce_telegram_bot_token=t.smbcommerce_telegram_bot_token);const L=await fetch(`${x}/admin/settings`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify(c)});if(!L.ok){const X=await L.json().catch(()=>null);throw new Error((X==null?void 0:X.message)||"Save failed")}b("Settings saved successfully!"),s(X=>({...X,smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:""})),de()}catch(c){const L=c instanceof Error&&c.message!=="Save failed"?` ${c.message}`:"";b(`Error saving settings.${L}`),console.error(c)}finally{i(!1),setTimeout(()=>b(""),8e3)}},ze=()=>{r.ask("Restore the default playbook? Your current text in this field will be replaced.","Restore default",je)},je=async()=>{try{const c=await(await fetch(`${x}/admin/playbook/default`,{headers:N.getHeaders()})).json();s(L=>({...L,smbcommerce_sales_playbook:c.playbook||""})),b("Default playbook loaded. Click Save to apply."),setTimeout(()=>b(""),4e3)}catch{b("Could not load the default playbook.")}},Ue=async()=>{B(!0),D(null);try{const c=await(await fetch(`${x}/admin/products/sync`,{method:"POST",headers:N.getHeaders(),body:JSON.stringify({})})).json();D(c),he()}catch(n){D({error:n.message})}finally{B(!1)}},he=async()=>{try{const n=await fetch(`${x}/admin/products/status`,{headers:N.getHeaders()});u(await n.json())}catch{}},Ve=async()=>{w(!0),E(null);try{const c=await(await fetch(`${x}/admin/ai-test`,{headers:N.getHeaders()})).json();E(c)}catch(n){E({error:n.message})}finally{w(!1)}},qe=()=>{const n=window.wp;if(!n||!n.media){b("The WordPress media library is not available on this page. Paste the logo URL instead."),setTimeout(()=>b(""),8e3);return}const c=n.media({title:"Select Logo",button:{text:"Use this logo"},multiple:!1});c.on("select",()=>{const L=c.state().get("selection").first().toJSON();s({...t,smbcommerce_logo_url:L.url})}),c.open()};return e("div",{className:"smb-commerce-settings-container",children:[f&&e("div",{className:"smb-commerce-notice",role:"status",children:e("p",{children:f})}),e(me,{request:r.request,onCancel:r.cancel}),e("div",{className:"smb-commerce-settings-layout",children:[e("div",{className:"smb-commerce-settings-form-col",children:e("form",{onSubmit:Fe,className:"smb-commerce-form",children:[e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-art"})," Widget appearance"]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Brand name"}),e("input",{type:"text",value:t.smbcommerce_brand_name,onChange:n=>s({...t,smbcommerce_brand_name:n.target.value})}),e("p",{className:"description",children:"The name shown in the chat window header."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Font"}),e("select",{value:t.smbcommerce_font_family,onChange:n=>s({...t,smbcommerce_font_family:n.target.value}),children:[e("option",{value:"system",children:"System font (no external requests)"}),e("option",{value:"Inter",children:"Inter (Google Fonts)"}),e("option",{value:"Roboto",children:"Roboto (Google Fonts)"}),e("option",{value:"Open Sans",children:"Open Sans (Google Fonts)"}),e("option",{value:"Montserrat",children:"Montserrat (Google Fonts)"}),e("option",{value:"Poppins",children:"Poppins (Google Fonts)"})]}),e("p",{className:"description",children:"Google Fonts are loaded from fonts.googleapis.com in your visitors' browsers."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Logo URL"}),e("div",{style:{display:"flex",gap:"8px"},children:[e("input",{type:"text",value:t.smbcommerce_logo_url,onChange:n=>s({...t,smbcommerce_logo_url:n.target.value}),placeholder:"https://...",style:{flex:1}}),e("button",{type:"button",className:"button",onClick:qe,children:"Upload image"})]}),e("p",{className:"description",children:"Shown on the left of the widget header."})]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Primary colour"}),e("div",{className:"color-picker-wrapper",children:[e("input",{type:"color",value:t.smbcommerce_primary_color,onChange:n=>s({...t,smbcommerce_primary_color:n.target.value})}),e("span",{className:"color-hex",children:t.smbcommerce_primary_color})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Footer tagline"}),e("input",{type:"text",placeholder:"AI sales assistant, available 24/7",value:t.smbcommerce_widget_slogan,onChange:n=>s({...t,smbcommerce_widget_slogan:n.target.value})})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Other ways to reach you"}),e("p",{className:"description",children:"Shown as small icons under the chat box. Leave a field empty to hide that channel."}),e("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"10px",marginTop:"5px"},children:[e("input",{type:"text","aria-label":"WhatsApp number or link",placeholder:"WhatsApp number (e.g. +1 555 0100)",value:t.smbcommerce_whatsapp||"",onChange:n=>s({...t,smbcommerce_whatsapp:n.target.value})}),e("input",{type:"text","aria-label":"Messenger link",placeholder:"Messenger link (e.g. https://m.me/...)",value:t.smbcommerce_messenger_link||"",onChange:n=>s({...t,smbcommerce_messenger_link:n.target.value})}),e("input",{type:"text","aria-label":"Telegram username or link",placeholder:"Telegram (e.g. @yourshop)",value:t.smbcommerce_telegram_link||"",onChange:n=>s({...t,smbcommerce_telegram_link:n.target.value})}),e("input",{type:"text","aria-label":"Zalo link",placeholder:"Zalo link (e.g. https://zalo.me/...)",value:t.smbcommerce_zalo_link||"",onChange:n=>s({...t,smbcommerce_zalo_link:n.target.value})}),e("input",{type:"text","aria-label":"Phone number",placeholder:"Phone (e.g. +1 555 0100)",value:t.smbcommerce_hotline||"",onChange:n=>s({...t,smbcommerce_hotline:n.target.value})})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_ai_notice_enabled==="1",onChange:n=>s({...t,smbcommerce_ai_notice_enabled:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Tell visitors they are chatting with AI"}),e("p",{className:"description",style:{margin:0},children:"Shows a short line at the top of the chat. Recommended, and expected by AI transparency rules in many countries."})]})]}),t.smbcommerce_ai_notice_enabled==="1"&&e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-ai-notice",children:"AI notice text"}),e("input",{id:"smb-ai-notice",type:"text",maxLength:200,placeholder:"You are chatting with an AI assistant. A team member can join when needed.",value:t.smbcommerce_ai_notice_text,onChange:n=>s({...t,smbcommerce_ai_notice_text:n.target.value})}),e("p",{className:"description",children:"Leave empty to use the default text, translated into the site language."})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_hide_on_checkout==="1",onChange:n=>s({...t,smbcommerce_hide_on_checkout:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Hide the chat on the checkout page"}),e("p",{className:"description",style:{margin:0},children:"Off by default. The chat button already moves up on cart and checkout pages so it does not cover the order button."})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_voice_input_enabled==="1",onChange:n=>s({...t,smbcommerce_voice_input_enabled:n.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let visitors speak their message"}),e("p",{className:"description",style:{margin:0},children:"Adds a microphone button. The visitor's browser turns speech into text in the message box, and the visitor checks it before sending. The plugin never receives or stores audio, but depending on the browser the audio may be sent to the browser maker's speech service (for example Google for Chrome, Apple for Safari). Shown only on HTTPS pages in browsers that support it (not Firefox). Off by default."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Suggested question chips"}),e("textarea",{style:{width:"100%",height:"80px",padding:"12px"},placeholder:`One per line. For example:
     1import{u as e,S as D,d,A as Z,h as F}from"./assets/hooks.module-C33f2O-M.js";import{p as $e,n as Be,u as oe,a as ce,b as w,N as ie,C as le,B as H,t as be}from"./assets/i18n-D_zElFbw.js";function Ne(r){return{render:function(t){Be(t,r)},unmount:function(){$e(r)}}}function _e({card:r}){const t=r.orders_capped?`${r.orders_count}+ orders`:`${r.orders_count} ${r.orders_count===1?"order":"orders"}`;return e("div",{className:"smb-customer-card",children:[e("h4",{children:r.name||(r.guest?"Guest customer":"Customer")}),r.other_customers>0&&e("p",{className:"smb-customer-warn",children:"Another customer was also verified in this chat. This card shows the most recent one."}),e("dl",{children:[e("dt",{children:"Orders"}),e("dd",{children:t}),e("dt",{children:"Total spent"}),e("dd",{children:r.total_spent.length?r.total_spent.map(n=>n.text).join(" · "):"No paid orders yet"}),r.last_order&&e(D,{children:[e("dt",{children:"Last order"}),e("dd",{children:[r.last_order.url?e("a",{href:r.last_order.url,children:["#",r.last_order.number]}):e(D,{children:["#",r.last_order.number]})," · ",r.last_order.status_label,e("div",{className:"smb-customer-sub",children:[r.last_order.date," · ",r.last_order.total_text]})]})]}),r.recent_products.length>0&&e(D,{children:[e("dt",{children:"Recently bought"}),e("dd",{children:e("ul",{children:r.recent_products.map((n,p)=>e("li",{children:n.url?e("a",{href:n.url,target:"_blank",rel:"noopener noreferrer",children:n.name}):n.name},p))})})]})]}),(r.links.orders||r.links.profile)&&e("p",{className:"smb-customer-links",children:[r.links.orders&&e("a",{href:r.links.orders,children:"All orders"}),r.links.orders&&r.links.profile&&" · ",r.links.profile&&e("a",{href:r.links.profile,children:"Customer profile"})]}),e("p",{className:"smb-customer-note",children:[r.guest?"Guest checkout: only the order verified in this chat is shown. ":"","Read-only, from WooCommerce. Not shared with the AI."]})]})}const Oe={bot_active:"AI assistant",human_active:"Waiting for your team",awaiting_agent:"Waiting for your team",agent_active:"Team member replying",closed:"Closed"},Pe={webchat:"Website chat"},ge=r=>Oe[r]||r,We=r=>r.sender_type==="user"?"Customer":r.sender_type==="agent"?r.first_name?`${r.first_name} (team)`:"Team member":r.sender_type==="system"?"Assistant":r.sender_type;function De({openId:r=null}={}){const{notices:t,push:n,dismiss:p}=oe(),m=ce(),[u,k]=d([]),[h,g]=d(null),[x,T]=d([]),[_,N]=d(""),[C,I]=d(new Set),[L,$]=d(!1),v=Z(null),i=Z(5e3),l=Z(null),R=Z(null),[B,P]=d(null),O=Z(null),M=o=>{w.get(`/admin/conversations/${o}/customer`).then(f=>{O.current===o&&P((f==null?void 0:f.card)||null)}).catch(()=>{O.current===o&&P(null)})};F(()=>{S()},[]),F(()=>{r&&w.get(`/admin/conversations/${r}`).then(o=>o&&U(o)).catch(()=>n("error","This conversation no longer exists."))},[r]);const S=async()=>{try{const o=await w.getConversations();k(o.items||[])}catch(o){console.error(o)}},U=async o=>{var f;g(o),O.current=o.id,P(null),M(o.id);try{const y=await w.getMessages(o.id);T(y.messages||[]),R.current=((f=y.messages)==null?void 0:f.length)>0?y.messages[y.messages.length-1].id:null,q()}catch(y){console.error(y)}},q=()=>setTimeout(()=>{var o;return(o=v.current)==null?void 0:o.scrollIntoView({behavior:"smooth"})},100),G=async()=>{var o;if(!h||document.visibilityState!=="visible"){l.current=window.setTimeout(G,i.current);return}try{const f=await w.getMessages(h.id,R.current);((o=f.messages)==null?void 0:o.length)>0&&(T(y=>{const A=new Set(f.messages.map(z=>z.id));return[...y.filter(z=>!A.has(z.id)),...f.messages]}),R.current=f.messages[f.messages.length-1].id,q(),i.current=5e3,M(h.id))}catch{i.current=Math.min(i.current*2,6e4)}l.current=window.setTimeout(G,i.current)};F(()=>(h&&(l.current=window.setTimeout(G,i.current)),()=>{l.current&&clearTimeout(l.current)}),[h]);const b=async()=>{if(h)try{await w.takeover(h.id),g({...h,status:"agent_active"}),S()}catch{n("error","Takeover failed. Another agent may already have this conversation.")}},J=async()=>{if(h)try{await w.release(h.id),g({...h,status:"bot_active"}),S()}catch{n("error","Could not hand the conversation back to the bot.")}},K=(o,f)=>{f.stopPropagation(),m.ask(`Permanently delete conversation #${o}? This cannot be undone.`,"Delete",async()=>{try{await w.deleteConversation(o),(h==null?void 0:h.id)===o&&(g(null),T([]),P(null),O.current=null),I(y=>{const A=new Set(y);return A.delete(o),A}),S(),n("success",`Conversation #${o} deleted.`)}catch{n("error","Could not delete the conversation.")}})},te=(o,f)=>{f.stopPropagation(),I(y=>{const A=new Set(y);return A.has(o)?A.delete(o):A.add(o),A})},se=()=>{C.size===u.length&&u.length>0?I(new Set):I(new Set(u.map(o=>o.id)))},re=()=>{if(C.size===0)return;const o=C.size;m.ask(`Permanently delete ${o} selected conversation(s)? This cannot be undone.`,`Delete ${o}`,async()=>{$(!0);try{const f=await w.bulkDeleteConversations(Array.from(C));h&&C.has(h.id)&&(g(null),T([]),P(null),O.current=null),I(new Set),S(),f.failed>0?n("warning",`${f.failed} conversation(s) could not be deleted.`):n("success",`${o} conversation(s) deleted.`)}catch{n("error","Bulk delete failed.")}finally{$(!1)}})},W=async()=>{if(!_.trim()||!h)return;const o=_.trim();N("");const f=crypto.randomUUID(),y={id:-Date.now(),sender_type:"agent",content:o,created_at:new Date().toISOString()};T(A=>[...A,y]),q();try{const A=await w.sendMessage(h.id,o,f);T(z=>z.map(X=>X.id===y.id?A:X)),R.current=A.id}catch{n("error","Could not send the message. Your text is back in the reply box."),T(A=>A.filter(z=>z.id!==y.id)),N(o)}},Q={bot_active:"#10b981",human_active:"#f59e0b",agent_active:"#3b82f6",closed:"#9ca3af"};return e(D,{children:[e(ie,{notices:t,onDismiss:p}),e(le,{request:m.request,onCancel:m.cancel}),e("div",{className:"smb-commerce-inbox-container",children:[e("div",{className:"smb-commerce-sidebar",children:[e("div",{style:{padding:"12px 15px",borderBottom:"1px solid #ccd0d4",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("h2",{style:{margin:0,fontSize:"15px",fontWeight:700},children:["Conversations (",u.length,")"]}),e("button",{className:"button",style:{fontSize:"12px",padding:"3px 8px"},onClick:se,children:C.size===u.length&&u.length>0?"Deselect all":"Select all"})]}),C.size>0&&e("div",{style:{padding:"8px 12px",background:"#fff3cd",borderBottom:"1px solid #f0c36d",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e("span",{style:{fontSize:"13px",fontWeight:600,color:"#856404"},children:["Selected: ",C.size]}),e("button",{style:{background:"#d63638",color:"white",border:"none",fontSize:"12px",padding:"4px 12px",borderRadius:"4px",cursor:"pointer",fontWeight:600},onClick:re,disabled:L,children:L?"Deleting…":"Delete "+C.size})]}),e("ul",{className:"smb-commerce-conversation-list",style:{listStyle:"none",margin:0,padding:0},children:u.map(o=>e("li",{className:"smb-commerce-conversation-item "+((h==null?void 0:h.id)===o.id?"active":""),onClick:()=>U(o),style:{display:"flex",alignItems:"center",gap:"8px",padding:"10px 12px",background:C.has(o.id)?"#eff6ff":void 0},children:[e("input",{type:"checkbox",checked:C.has(o.id),onClick:f=>te(o.id,f),onChange:()=>{},style:{width:"15px",height:"15px",flexShrink:0,cursor:"pointer"}}),e("div",{style:{flex:1,minWidth:0},children:[e("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"4px"},children:[e("span",{style:{fontWeight:600,fontSize:"13px"},children:["Conversation #",o.id]}),e("span",{style:{fontSize:"10px",fontWeight:600,color:"white",background:Q[o.status]||"#9ca3af",padding:"1px 6px",borderRadius:"10px",whiteSpace:"nowrap"},children:ge(o.status)})]}),e("div",{style:{fontSize:"11px",color:"#666",marginTop:"2px"},children:Pe[o.channel]||o.channel})]}),e("button",{style:{color:"#d63638",padding:"2px",background:"none",border:"none",cursor:"pointer",flexShrink:0},onClick:f=>K(o.id,f),title:"Delete conversation","aria-label":`Delete conversation #${o.id}`,children:e("span",{className:"dashicons dashicons-trash",style:{fontSize:"16px"}})})]},o.id))})]}),e("div",{className:"smb-commerce-main",children:h?e(D,{children:[e("div",{className:"smb-commerce-main-header",children:[e("h3",{children:["Conversation #",h.id," — ",e("span",{style:{color:Q[h.status]},children:ge(h.status)})]}),e("div",{className:"actions",children:[h.status!=="agent_active"&&e("button",{onClick:b,className:"button button-primary",children:"Take over"}),h.status==="agent_active"&&e("button",{onClick:J,className:"button button-secondary",children:"Hand back to bot"})]})]}),B&&e("details",{className:"smb-customer-inline",children:[e("summary",{children:["Customer: ",B.name||"Guest customer"]}),e(_e,{card:B})]}),e("div",{className:"smb-commerce-chat-history",children:[x.map(o=>e("div",{className:"smb-commerce-admin-msg "+o.sender_type,children:[e("div",{className:"msg-sender",children:We(o)}),e("div",{className:"msg-content",children:o.content})]},o.id)),e("div",{ref:v})]}),e("div",{className:"smb-commerce-main-input",children:[e("input",{type:"text",value:_,onInput:o=>N(o.target.value),onKeyPress:o=>o.key==="Enter"&&W(),placeholder:"Type your reply…",disabled:h.status!=="agent_active"&&h.status!=="awaiting_agent"}),e("button",{className:"button button-primary",onClick:W,disabled:h.status!=="agent_active"&&h.status!=="awaiting_agent",children:"Send"})]})]}):e("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",color:"#666"},children:"Select a conversation to get started"})}),h&&B&&e("aside",{className:"smb-customer-aside","aria-label":"Customer",children:e(_e,{card:B})})]})]})}const Fe={"message.created":"Visitor sends a message","conversation.created":"Visitor opens the chat"},Me={content:"Message text",time:"Site hour"},ye={soft:"normal priority",hard:"high priority",emergency:"urgent"},xe=r=>{if(typeof r!="string")return r||{};try{return JSON.parse(r)||{}}catch{return{}}},ze=r=>{const t=xe(r),n=t.conditions;if(!Array.isArray(n)||n.length===0)return"Always";const p=m=>String(m??"").split(",").map(u=>u.trim()).filter(Boolean).map(u=>`"${u}"`).join(" or ");return n.map(m=>{if(m.type==="message_contains")return`Message text contains "${m.value}"`;if(m.type==="conversation.has_tag")return`Conversation has the tag "${m.value}"`;const u=Me[m.field]||m.field;return m.operator==="contains"?`${u} contains ${p(m.value)}`:m.operator==="equals"?`${u} is exactly "${m.value}"`:m.operator==="after"?`${u} is ${m.value}:00 or later`:JSON.stringify(m)}).join(t.type==="or"?" or ":" and ")},He=r=>{const t=xe(r).actions;return!Array.isArray(t)||t.length===0?JSON.stringify(r):t.map(n=>n.type==="handoff"?`Hand off to a person (${ye[String(n.value||"").toLowerCase()]||ye.soft})`:n.type==="send_message"?`Reply: "${n.value}"`:n.type==="tag_conversation"?`Add the tag "${n.value}"`:JSON.stringify(n)).join(", then ")},je=()=>{const{notices:r,push:t,dismiss:n}=oe(),p=ce(),[m,u]=d(""),[k,h]=d([]),[g,x]=d(!0),[T,_]=d(!1),[N,C]=d(""),[I,L]=d("message.created"),[$,v]=d("content"),[i,l]=d("contains"),[R,B]=d(""),[P,O]=d("handoff"),[M,S]=d("Soft"),U=async()=>{x(!0);try{const b=await w.get("/admin/rules");b.items&&h(b.items)}catch(b){console.error("Failed to load rules",b)}x(!1)};F(()=>{U()},[]);const q=(b,J)=>{p.ask(`Delete the rule "${J}"?`,"Delete rule",async()=>{try{await w.delete(`/admin/rules/${b}`),U(),t("success","Rule deleted.")}catch{t("error","Could not delete the rule.")}})},G=async()=>{if(!N.trim()){u("Please enter a rule name.");return}u("");const b={name:N,trigger_event:I,conditions:{type:"and",conditions:[{type:"simple",field:$,operator:i,value:R}]},actions:{actions:[{type:P,value:M}]},priority:10,is_active:1};try{await w.post("/admin/rules",b),_(!1),C(""),B(""),U(),t("success","Rule saved and enabled.")}catch{t("error","Could not save the rule.")}};return g?e("div",{role:"status",children:"Loading…"}):e("div",{className:"smb-commerce-rules",children:[e(ie,{notices:r,onDismiss:n}),e(le,{request:p.request,onCancel:p.cancel}),e("h2",{children:"Automation rules"}),e("p",{children:"Automatically run an action when a visitor sends a message or opens the chat, based on conditions you define."}),T?e("div",{className:"rule-form",style:{background:"#fff",padding:"20px",border:"1px solid #ccd0d4",borderRadius:"4px",maxWidth:"800px"},children:[e("h3",{style:{marginTop:0},children:"Create an automation rule"}),e("div",{style:{marginBottom:15},children:[e("label",{htmlFor:"smb-rule-name",style:{fontWeight:600},children:"Rule name:"}),e("br",{}),e("input",{id:"smb-rule-name",type:"text",className:"regular-text",style:{width:"100%",maxWidth:"400px"},value:N,onChange:b=>{C(b.target.value),m&&u("")},"aria-invalid":m?!0:void 0,"aria-describedby":m?"smb-rule-name-error":void 0,placeholder:"e.g. Hand off when a visitor complains"}),m&&e("p",{id:"smb-rule-name-error",className:"smb-field-error",role:"alert",children:m})]}),e("div",{style:{marginBottom:15},children:[e("label",{style:{fontWeight:600},children:"1. When this happens (trigger):"}),e("br",{}),e("select",{value:I,onChange:b=>L(b.target.value),style:{width:"100%",maxWidth:"400px"},children:[e("option",{value:"message.created",children:"Visitor sends a new message"}),e("option",{value:"conversation.created",children:"Visitor opens the chat for the first time"})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #2271b1"},children:[e("label",{style:{fontWeight:600},children:"2. AND these conditions match (if):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:$,onChange:b=>v(b.target.value),children:[e("option",{value:"content",children:"Message text"}),e("option",{value:"time",children:"Current time (site hour, 0–23)"})]}),e("select",{value:i,onChange:b=>l(b.target.value),children:[e("option",{value:"contains",children:"Contains keyword"}),e("option",{value:"equals",children:"Exactly matches"}),e("option",{value:"after",children:"After (hour)"})]}),e("input",{type:"text",value:R,onChange:b=>B(b.target.value),placeholder:"e.g. refund, quote..."})]})]}),e("div",{style:{marginBottom:15,padding:"15px",background:"#f6f7f7",borderLeft:"4px solid #00a32a"},children:[e("label",{style:{fontWeight:600},children:"3. THEN do this (then):"}),e("br",{}),e("div",{style:{display:"flex",gap:"10px",marginTop:"10px"},children:[e("select",{value:P,onChange:b=>O(b.target.value),children:[e("option",{value:"handoff",children:"Hand off to a human agent"}),e("option",{value:"send_message",children:"Send an automatic message"}),e("option",{value:"tag_conversation",children:"Add a tag"})]}),e("input",{type:"text",value:M,onChange:b=>S(b.target.value),placeholder:"e.g. Soft (or the message text)",style:{flex:1}})]})]}),e("div",{style:{marginTop:20,paddingTop:"15px",borderTop:"1px solid #ddd"},children:[e("button",{onClick:G,className:"button button-primary",children:"Save and enable"}),e("button",{onClick:()=>_(!1),className:"button",style:{marginLeft:10},children:"Cancel"})]})]}):e(D,{children:[e("button",{onClick:()=>_(!0),className:"button button-primary",style:{marginBottom:15},children:"+ New rule"}),e("table",{className:"wp-list-table widefat fixed striped",children:[e("thead",{children:e("tr",{children:[e("th",{children:"Rule name"}),e("th",{children:"When"}),e("th",{children:"If"}),e("th",{children:"Then"}),e("th",{children:"Status"}),e("th",{children:"Actions"})]})}),e("tbody",{children:[k.map(b=>e("tr",{children:[e("td",{children:e("strong",{children:b.name})}),e("td",{children:Fe[b.trigger_event]||b.trigger_event}),e("td",{children:ze(b.conditions)}),e("td",{children:He(b.actions)}),e("td",{children:b.is_active?e("span",{style:{color:"green"},children:"Active"}):e("span",{style:{color:"gray"},children:"Disabled"})}),e("td",{children:e("button",{type:"button",onClick:()=>q(b.id,b.name),className:"button-link button-link-delete",children:"Delete"})})]},b.id)),k.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No rules created yet."})})]})]})]})]})},Ue=()=>{const[r,t]=d([]),[n,p]=d(!0),m=async()=>{p(!0);try{const k=await w.getLeads();k.items&&t(k.items)}catch(k){console.error("Failed to load leads",k)}p(!1)};return F(()=>{m()},[]),n?e("div",{children:"Loading leads..."}):e("div",{className:"smb-commerce-leads",children:[e("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e("h2",{children:"Leads"}),e("button",{className:"button button-primary",onClick:()=>{var h,g;const k=`${((h=window.SMBCommerceAdmin)==null?void 0:h.restUrl)||"/wp-json/smb-commerce/v1"}/admin/leads/export?_wpnonce=${((g=window.SMBCommerceAdmin)==null?void 0:g.nonce)||""}`;window.location.href=k},children:[e("span",{className:"dashicons dashicons-download",style:{marginTop:"4px"}})," Export CSV"]})]}),e("table",{className:"wp-list-table widefat fixed striped",style:{marginTop:"10px"},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Name"}),e("th",{children:"Email"}),e("th",{children:"Phone"}),e("th",{children:"Score"}),e("th",{children:"Status"}),e("th",{children:"Created At"})]})}),e("tbody",{children:[r.map(k=>e("tr",{children:[e("td",{children:k.name||"-"}),e("td",{children:k.email||"-"}),e("td",{children:k.phone||"-"}),e("td",{children:k.score}),e("td",{children:k.status}),e("td",{children:new Date(k.created_at).toLocaleString()})]},k.id)),r.length===0&&e("tr",{children:e("td",{colSpan:6,children:"No leads found."})})]})]})]})};function qe(){var he,ue,pe;const r=ce(),[t,n]=d({smbcommerce_brand_name:"Shop Assistant",smbcommerce_primary_color:"#01696f",smbcommerce_logo_url:"",smbcommerce_font_family:"system",smbcommerce_runtime_mode:"smart_hybrid",smbcommerce_ai_provider:"openai",smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:"",smbcommerce_telegram_chat_id:"",smbcommerce_sales_playbook:"",smbcommerce_business_info:"",smbcommerce_enable_adaptive_playbook:"1",smbcommerce_meta_prompt_template:"",smbcommerce_enable_prompt_creator:"1",smbcommerce_prompt_creator_template:"",smbcommerce_max_upload_size:"5",smbcommerce_handoff_webhook:"",smbcommerce_system_guardrails_prompt:"",smbcommerce_media_retention_days:"30",smbcommerce_widget_slogan:"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:"",smbcommerce_hotline:"",smbcommerce_zalo_link:"",smbcommerce_messenger_link:"",smbcommerce_whatsapp:"",smbcommerce_telegram_link:"",smbcommerce_ai_notice_enabled:"1",smbcommerce_ai_notice_text:"",smbcommerce_hide_on_checkout:"0",smbcommerce_voice_input_enabled:"0",smbcommerce_reply_language:"customer",smbcommerce_coupon_allowlist:"",smbcommerce_tracking_code_meta:"",smbcommerce_tracking_carrier_meta:"",smbcommerce_tracking_carrier:"",smbcommerce_tracking_url_template:"",smbcommerce_abilities_enabled:"0",smbcommerce_gemini_model:"",smbcommerce_anthropic_model:"",smbcommerce_openai_model:"",smbcommerce_refund_policy:"",smbcommerce_warranty_policy:"",smbcommerce_delivery_policy:"",smbcommerce_privacy_policy:"",smbcommerce_enable_link_reading:"0",smbcommerce_budget_daily_usd:"0",smbcommerce_budget_monthly_usd:"0",smbcommerce_enable_provider_fallback:"1",smbcommerce_block_ungrounded_prices:"0",smbcommerce_enable_response_cache:"0",smbcommerce_cache_ttl_hours:"24",smbcommerce_webhook_url:"",smbcommerce_webhook_secret:""}),[p,m]=d({openai:!1,anthropic:!1,gemini:!1,telegram:!1}),[u,k]=d(!1),[h,g]=d(""),[x,T]=d(null),[_,N]=d(!1),[C,I]=d(!1),[L,$]=d(null),[v,i]=d(null),[l,R]=d(null),[B,P]=d(30),[O,M]=d(null),[S,U]=d(null),[q,G]=d({}),[b,J]=d(!0),[K,te]=d(!1),[se,re]=d(!1),[W,Q]=d(null),[o,f]=d(null),y=s=>!q[s]||q[s].allowed;F(()=>{me(),de()},[]),F(()=>{A(B)},[B]);const A=async s=>{try{const a=await fetch(`${H}/admin/analytics?days=${s}`,{headers:w.getHeaders()});R(await a.json())}catch(a){console.error("Analytics load failed",a)}},z=async()=>{M({pending:!0});try{const s=await fetch(`${H}/admin/webhook/test`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})});M(await s.json())}catch(s){M({message:s.message})}},X=async()=>{try{await fetch(`${H}/admin/cache/flush`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})}),A(B),g("Cache cleared."),setTimeout(()=>g(""),3e3)}catch{g("Could not clear the cache.")}},V=s=>"$"+(Number(s)||0).toFixed(s<1?4:2);F(()=>{const s=t.smbcommerce_font_family;if(!s||s==="system")return;const a=`smb-commerce-font-${s.replace(/\s+/g,"-")}`;if(!document.getElementById(a)){const E=document.createElement("link");E.id=a,E.rel="stylesheet",E.href=`https://fonts.googleapis.com/css2?family=${s.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(E)}},[t.smbcommerce_font_family]);const me=async()=>{try{const a=await(await fetch(`${H}/admin/settings`,{headers:w.getHeaders()})).json();a.settings&&(n(E=>({...E,smbcommerce_brand_name:a.settings.smbcommerce_brand_name||"",smbcommerce_primary_color:a.settings.smbcommerce_primary_color||"",smbcommerce_logo_url:a.settings.smbcommerce_logo_url||"",smbcommerce_font_family:a.settings.smbcommerce_font_family||"system",smbcommerce_runtime_mode:a.settings.smbcommerce_runtime_mode||"smart_hybrid",smbcommerce_ai_provider:a.settings.smbcommerce_ai_provider||"openai",smbcommerce_telegram_chat_id:a.settings.smbcommerce_telegram_chat_id||"",smbcommerce_sales_playbook:a.settings.smbcommerce_sales_playbook||"",smbcommerce_business_info:a.settings.smbcommerce_business_info||"",smbcommerce_enable_adaptive_playbook:a.settings.smbcommerce_enable_adaptive_playbook||"1",smbcommerce_meta_prompt_template:a.settings.smbcommerce_meta_prompt_template||"",smbcommerce_enable_prompt_creator:a.settings.smbcommerce_enable_prompt_creator||"1",smbcommerce_prompt_creator_template:a.settings.smbcommerce_prompt_creator_template||"",smbcommerce_max_upload_size:a.settings.smbcommerce_max_upload_size||"5",smbcommerce_handoff_webhook:a.settings.smbcommerce_handoff_webhook||"",smbcommerce_system_guardrails_prompt:a.settings.smbcommerce_system_guardrails_prompt||"",smbcommerce_media_retention_days:a.settings.smbcommerce_media_retention_days||"30",smbcommerce_widget_slogan:a.settings.smbcommerce_widget_slogan||"AI sales assistant, available 24/7",smbcommerce_suggestion_chips:a.settings.smbcommerce_suggestion_chips||"",smbcommerce_hotline:a.settings.smbcommerce_hotline||"",smbcommerce_zalo_link:a.settings.smbcommerce_zalo_link||"",smbcommerce_messenger_link:a.settings.smbcommerce_messenger_link||"",smbcommerce_whatsapp:a.settings.smbcommerce_whatsapp||"",smbcommerce_telegram_link:a.settings.smbcommerce_telegram_link||"",smbcommerce_ai_notice_enabled:a.settings.smbcommerce_ai_notice_enabled??"1",smbcommerce_ai_notice_text:a.settings.smbcommerce_ai_notice_text||"",smbcommerce_hide_on_checkout:a.settings.smbcommerce_hide_on_checkout??"0",smbcommerce_voice_input_enabled:a.settings.smbcommerce_voice_input_enabled??"0",smbcommerce_reply_language:a.settings.smbcommerce_reply_language||"customer",smbcommerce_coupon_allowlist:a.settings.smbcommerce_coupon_allowlist||"",smbcommerce_tracking_code_meta:a.settings.smbcommerce_tracking_code_meta||"",smbcommerce_tracking_carrier_meta:a.settings.smbcommerce_tracking_carrier_meta||"",smbcommerce_tracking_carrier:a.settings.smbcommerce_tracking_carrier||"",smbcommerce_tracking_url_template:a.settings.smbcommerce_tracking_url_template||"",smbcommerce_abilities_enabled:a.settings.smbcommerce_abilities_enabled??"0",smbcommerce_gemini_model:a.settings.smbcommerce_gemini_model||"",smbcommerce_anthropic_model:a.settings.smbcommerce_anthropic_model||"",smbcommerce_openai_model:a.settings.smbcommerce_openai_model||"",smbcommerce_refund_policy:a.settings.smbcommerce_refund_policy||"",smbcommerce_warranty_policy:a.settings.smbcommerce_warranty_policy||"",smbcommerce_delivery_policy:a.settings.smbcommerce_delivery_policy||"",smbcommerce_privacy_policy:a.settings.smbcommerce_privacy_policy||"",smbcommerce_enable_link_reading:a.settings.smbcommerce_enable_link_reading||"0",smbcommerce_budget_daily_usd:a.settings.smbcommerce_budget_daily_usd||"0",smbcommerce_budget_monthly_usd:a.settings.smbcommerce_budget_monthly_usd||"0",smbcommerce_enable_provider_fallback:a.settings.smbcommerce_enable_provider_fallback||"1",smbcommerce_block_ungrounded_prices:a.settings.smbcommerce_block_ungrounded_prices||"0",smbcommerce_enable_response_cache:a.settings.smbcommerce_enable_response_cache||"0",smbcommerce_cache_ttl_hours:a.settings.smbcommerce_cache_ttl_hours||"24",smbcommerce_webhook_url:a.settings.smbcommerce_webhook_url||"",smbcommerce_webhook_secret:""})),U(a.settings.smbcommerce_license||null),G(a.settings.smbcommerce_features||{}),J(a.settings.smbcommerce_premium_build!==!1),te(a.settings.smbcommerce_wp_ai_available===!0),re(a.settings.smbcommerce_abilities_supported===!0),Q(a.settings.smbcommerce_model_report||null),f(a.settings.smbcommerce_business_info_is_sample?a.settings.smbcommerce_business_info:null),m({openai:a.settings.smbcommerce_openai_key_exists,anthropic:a.settings.smbcommerce_anthropic_key_exists,gemini:a.settings.smbcommerce_gemini_key_exists,telegram:a.settings.smbcommerce_telegram_bot_token_exists}))}catch(s){console.error("Failed to load settings",s)}},Ae=async s=>{s.preventDefault(),k(!0),g("");try{const a={smbcommerce_brand_name:t.smbcommerce_brand_name,smbcommerce_primary_color:t.smbcommerce_primary_color,smbcommerce_logo_url:t.smbcommerce_logo_url,smbcommerce_font_family:t.smbcommerce_font_family,smbcommerce_runtime_mode:t.smbcommerce_runtime_mode,smbcommerce_ai_provider:t.smbcommerce_ai_provider,smbcommerce_telegram_chat_id:t.smbcommerce_telegram_chat_id,smbcommerce_sales_playbook:t.smbcommerce_sales_playbook,smbcommerce_business_info:t.smbcommerce_business_info,smbcommerce_enable_adaptive_playbook:t.smbcommerce_enable_adaptive_playbook,smbcommerce_meta_prompt_template:t.smbcommerce_meta_prompt_template,smbcommerce_enable_prompt_creator:t.smbcommerce_enable_prompt_creator,smbcommerce_prompt_creator_template:t.smbcommerce_prompt_creator_template,smbcommerce_max_upload_size:t.smbcommerce_max_upload_size,smbcommerce_handoff_webhook:t.smbcommerce_handoff_webhook,smbcommerce_system_guardrails_prompt:t.smbcommerce_system_guardrails_prompt,smbcommerce_media_retention_days:t.smbcommerce_media_retention_days,smbcommerce_widget_slogan:t.smbcommerce_widget_slogan,smbcommerce_suggestion_chips:t.smbcommerce_suggestion_chips,smbcommerce_hotline:t.smbcommerce_hotline,smbcommerce_zalo_link:t.smbcommerce_zalo_link,smbcommerce_messenger_link:t.smbcommerce_messenger_link,smbcommerce_whatsapp:t.smbcommerce_whatsapp,smbcommerce_telegram_link:t.smbcommerce_telegram_link,smbcommerce_ai_notice_enabled:t.smbcommerce_ai_notice_enabled,smbcommerce_ai_notice_text:t.smbcommerce_ai_notice_text,smbcommerce_hide_on_checkout:t.smbcommerce_hide_on_checkout,smbcommerce_voice_input_enabled:t.smbcommerce_voice_input_enabled,smbcommerce_reply_language:t.smbcommerce_reply_language,smbcommerce_coupon_allowlist:t.smbcommerce_coupon_allowlist,smbcommerce_tracking_code_meta:t.smbcommerce_tracking_code_meta,smbcommerce_tracking_carrier_meta:t.smbcommerce_tracking_carrier_meta,smbcommerce_tracking_carrier:t.smbcommerce_tracking_carrier,smbcommerce_tracking_url_template:t.smbcommerce_tracking_url_template,smbcommerce_abilities_enabled:t.smbcommerce_abilities_enabled,smbcommerce_gemini_model:t.smbcommerce_gemini_model,smbcommerce_anthropic_model:t.smbcommerce_anthropic_model,smbcommerce_openai_model:t.smbcommerce_openai_model,smbcommerce_refund_policy:t.smbcommerce_refund_policy,smbcommerce_warranty_policy:t.smbcommerce_warranty_policy,smbcommerce_delivery_policy:t.smbcommerce_delivery_policy,smbcommerce_privacy_policy:t.smbcommerce_privacy_policy,smbcommerce_enable_link_reading:t.smbcommerce_enable_link_reading,smbcommerce_budget_daily_usd:t.smbcommerce_budget_daily_usd,smbcommerce_budget_monthly_usd:t.smbcommerce_budget_monthly_usd,smbcommerce_enable_provider_fallback:t.smbcommerce_enable_provider_fallback,smbcommerce_block_ungrounded_prices:t.smbcommerce_block_ungrounded_prices,smbcommerce_enable_response_cache:t.smbcommerce_enable_response_cache,smbcommerce_cache_ttl_hours:t.smbcommerce_cache_ttl_hours,smbcommerce_webhook_url:t.smbcommerce_webhook_url};t.smbcommerce_webhook_secret&&(a.smbcommerce_webhook_secret=t.smbcommerce_webhook_secret),t.smbcommerce_openai_key&&(a.smbcommerce_openai_key=t.smbcommerce_openai_key),t.smbcommerce_anthropic_key&&(a.smbcommerce_anthropic_key=t.smbcommerce_anthropic_key),t.smbcommerce_gemini_key&&(a.smbcommerce_gemini_key=t.smbcommerce_gemini_key),t.smbcommerce_telegram_bot_token&&(a.smbcommerce_telegram_bot_token=t.smbcommerce_telegram_bot_token);const E=await fetch(`${H}/admin/settings`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify(a)});if(!E.ok){const Y=await E.json().catch(()=>null);throw new Error((Y==null?void 0:Y.message)||"Save failed")}g("Settings saved successfully!"),n(Y=>({...Y,smbcommerce_openai_key:"",smbcommerce_anthropic_key:"",smbcommerce_gemini_key:"",smbcommerce_telegram_bot_token:""})),me()}catch(a){const E=a instanceof Error&&a.message!=="Save failed"?` ${a.message}`:"";g(`Error saving settings.${E}`),console.error(a)}finally{k(!1),setTimeout(()=>g(""),8e3)}},Te=()=>{r.ask("Restore the default playbook? Your current text in this field will be replaced.","Restore default",Ie)},Ie=async()=>{try{const a=await(await fetch(`${H}/admin/playbook/default`,{headers:w.getHeaders()})).json();n(E=>({...E,smbcommerce_sales_playbook:a.playbook||""})),g("Default playbook loaded. Click Save to apply."),setTimeout(()=>g(""),4e3)}catch{g("Could not load the default playbook.")}},Le=async()=>{I(!0),$(null);try{const a=await(await fetch(`${H}/admin/products/sync`,{method:"POST",headers:w.getHeaders(),body:JSON.stringify({})})).json();$(a),de()}catch(s){$({error:s.message})}finally{I(!1)}},de=async()=>{try{const s=await fetch(`${H}/admin/products/status`,{headers:w.getHeaders()});i(await s.json())}catch{}},Re=async()=>{N(!0),T(null);try{const a=await(await fetch(`${H}/admin/ai-test`,{headers:w.getHeaders()})).json();T(a)}catch(s){T({error:s.message})}finally{N(!1)}},Ee=()=>{const s=window.wp;if(!s||!s.media){g("The WordPress media library is not available on this page. Paste the logo URL instead."),setTimeout(()=>g(""),8e3);return}const a=s.media({title:"Select Logo",button:{text:"Use this logo"},multiple:!1});a.on("select",()=>{const E=a.state().get("selection").first().toJSON();n({...t,smbcommerce_logo_url:E.url})}),a.open()};return e("div",{className:"smb-commerce-settings-container",children:[h&&e("div",{className:"smb-commerce-notice",role:"status",children:e("p",{children:h})}),e(le,{request:r.request,onCancel:r.cancel}),e("div",{className:"smb-commerce-settings-layout",children:[e("div",{className:"smb-commerce-settings-form-col",children:e("form",{onSubmit:Ae,className:"smb-commerce-form",children:[e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-art"})," Widget appearance"]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Brand name"}),e("input",{type:"text",value:t.smbcommerce_brand_name,onChange:s=>n({...t,smbcommerce_brand_name:s.target.value})}),e("p",{className:"description",children:"The name shown in the chat window header."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Font"}),e("select",{value:t.smbcommerce_font_family,onChange:s=>n({...t,smbcommerce_font_family:s.target.value}),children:[e("option",{value:"system",children:"System font (no external requests)"}),e("option",{value:"Inter",children:"Inter (Google Fonts)"}),e("option",{value:"Roboto",children:"Roboto (Google Fonts)"}),e("option",{value:"Open Sans",children:"Open Sans (Google Fonts)"}),e("option",{value:"Montserrat",children:"Montserrat (Google Fonts)"}),e("option",{value:"Poppins",children:"Poppins (Google Fonts)"})]}),e("p",{className:"description",children:"Google Fonts are loaded from fonts.googleapis.com in your visitors' browsers."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Logo URL"}),e("div",{style:{display:"flex",gap:"8px"},children:[e("input",{type:"text",value:t.smbcommerce_logo_url,onChange:s=>n({...t,smbcommerce_logo_url:s.target.value}),placeholder:"https://...",style:{flex:1}}),e("button",{type:"button",className:"button",onClick:Ee,children:"Upload image"})]}),e("p",{className:"description",children:"Shown on the left of the widget header."})]}),e("div",{className:"smb-commerce-form-group-row",children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Primary colour"}),e("div",{className:"color-picker-wrapper",children:[e("input",{type:"color",value:t.smbcommerce_primary_color,onChange:s=>n({...t,smbcommerce_primary_color:s.target.value})}),e("span",{className:"color-hex",children:t.smbcommerce_primary_color})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Footer tagline"}),e("input",{type:"text",placeholder:"AI sales assistant, available 24/7",value:t.smbcommerce_widget_slogan,onChange:s=>n({...t,smbcommerce_widget_slogan:s.target.value})})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Other ways to reach you"}),e("p",{className:"description",children:"Shown as small icons under the chat box. Leave a field empty to hide that channel."}),e("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(180px, 1fr))",gap:"10px",marginTop:"5px"},children:[e("input",{type:"text","aria-label":"WhatsApp number or link",placeholder:"WhatsApp number (e.g. +1 555 0100)",value:t.smbcommerce_whatsapp||"",onChange:s=>n({...t,smbcommerce_whatsapp:s.target.value})}),e("input",{type:"text","aria-label":"Messenger link",placeholder:"Messenger link (e.g. https://m.me/...)",value:t.smbcommerce_messenger_link||"",onChange:s=>n({...t,smbcommerce_messenger_link:s.target.value})}),e("input",{type:"text","aria-label":"Telegram username or link",placeholder:"Telegram (e.g. @yourshop)",value:t.smbcommerce_telegram_link||"",onChange:s=>n({...t,smbcommerce_telegram_link:s.target.value})}),e("input",{type:"text","aria-label":"Zalo link",placeholder:"Zalo link (e.g. https://zalo.me/...)",value:t.smbcommerce_zalo_link||"",onChange:s=>n({...t,smbcommerce_zalo_link:s.target.value})}),e("input",{type:"text","aria-label":"Phone number",placeholder:"Phone (e.g. +1 555 0100)",value:t.smbcommerce_hotline||"",onChange:s=>n({...t,smbcommerce_hotline:s.target.value})})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_ai_notice_enabled==="1",onChange:s=>n({...t,smbcommerce_ai_notice_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Tell visitors they are chatting with AI"}),e("p",{className:"description",style:{margin:0},children:"Shows a short line at the top of the chat. Recommended, and expected by AI transparency rules in many countries."})]})]}),t.smbcommerce_ai_notice_enabled==="1"&&e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-ai-notice",children:"AI notice text"}),e("input",{id:"smb-ai-notice",type:"text",maxLength:200,placeholder:"You are chatting with an AI assistant. A team member can join when needed.",value:t.smbcommerce_ai_notice_text,onChange:s=>n({...t,smbcommerce_ai_notice_text:s.target.value})}),e("p",{className:"description",children:"Leave empty to use the default text, translated into the site language."})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_hide_on_checkout==="1",onChange:s=>n({...t,smbcommerce_hide_on_checkout:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Hide the chat on the checkout page"}),e("p",{className:"description",style:{margin:0},children:"Off by default. The chat button already moves up on cart and checkout pages so it does not cover the order button."})]})]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_voice_input_enabled==="1",onChange:s=>n({...t,smbcommerce_voice_input_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let visitors speak their message"}),e("p",{className:"description",style:{margin:0},children:"Adds a microphone button. The visitor's browser turns speech into text in the message box, and the visitor checks it before sending. The plugin never receives or stores audio, but depending on the browser the audio may be sent to the browser maker's speech service (for example Google for Chrome, Apple for Safari). Shown only on HTTPS pages in browsers that support it (not Firefox). Off by default."})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Suggested question chips"}),e("textarea",{style:{width:"100%",height:"80px",padding:"12px"},placeholder:`One per line. For example:
    22Help me choose a plan
    33Do you ship internationally?
    4 What is your return policy?`,value:t.smbcommerce_suggestion_chips,onChange:n=>s({...t,smbcommerce_suggestion_chips:n.target.value})}),e("p",{className:"description",children:"One suggestion per line. Visitors can tap one to send that question instantly."})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-network"})," AI configuration"]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Assistant mode"}),e("select",{value:t.smbcommerce_runtime_mode,onChange:n=>s({...t,smbcommerce_runtime_mode:n.target.value}),children:[e("option",{value:"smart_hybrid",children:"Smart hybrid — answer locally where possible, use AI when needed (recommended)"}),e("option",{value:"local_only",children:"Local only — never send conversations to an AI provider"}),e("option",{value:"ai_enhanced",children:"AI enhanced — let the AI lead every reply"})]}),e("p",{className:"description",children:"Local only answers greetings, policy questions, cart and order lookups from your own store data and costs no tokens. It cannot answer anything else — those customers are asked to contact you directly. This setting covers AI providers only; Telegram notifications, webhooks and licensing still reach the internet."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI provider"}),e("select",{value:t.smbcommerce_ai_provider,onChange:n=>s({...t,smbcommerce_ai_provider:n.target.value}),children:[e("option",{value:"openai",children:"OpenAI (GPT-4o-mini)"}),e("option",{value:"anthropic",children:"Anthropic (Claude 3.5)"}),e("option",{value:"gemini",children:"Google Gemini"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI API Key"}),e("input",{type:"password",placeholder:a.openai?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_openai_key,onChange:n=>s({...t,smbcommerce_openai_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Anthropic API Key"}),e("input",{type:"password",placeholder:a.anthropic?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_anthropic_key,onChange:n=>s({...t,smbcommerce_anthropic_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini API Key"}),e("input",{type:"password",placeholder:a.gemini?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_gemini_key,onChange:n=>s({...t,smbcommerce_gemini_key:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini model"}),e("input",{type:"text",placeholder:"Leave empty for gemini-flash-latest",value:t.smbcommerce_gemini_model||"",onChange:n=>s({...t,smbcommerce_gemini_model:n.target.value})}),e("p",{className:"description",children:["Leave empty to always use Google's current Flash model. Enter a specific model (for example ",e("code",{children:"gemini-3.5-flash"}),") only if you need it."]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Claude Model"}),e("input",{type:"text",placeholder:"Leave empty for claude-sonnet-5-5",value:t.smbcommerce_anthropic_model,onChange:n=>s({...t,smbcommerce_anthropic_model:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI Model"}),e("input",{type:"text",placeholder:"Leave empty for gpt-4o-mini",value:t.smbcommerce_openai_model,onChange:n=>s({...t,smbcommerce_openai_model:n.target.value})})]}),W&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Models in use"}),["anthropic","openai","gemini"].filter(n=>{var c;return(c=W[n])==null?void 0:c.retired}).map(n=>e("div",{className:"notice notice-warning inline",children:e("p",{children:["The saved ",n==="anthropic"?"Claude":n==="openai"?"OpenAI":"Gemini"," model ",e("code",{children:W[n].configured})," is no longer available from the provider. The assistant is using ",e("code",{children:W[n].model})," instead. Enter a current model above to choose for yourself."]})},n)),((ue=W[t.smbcommerce_ai_provider])==null?void 0:ue.source)==="default"&&e("div",{className:"notice notice-info inline",children:e("p",{children:["No model is chosen for your main provider, so the assistant uses the default ",e("code",{children:W[t.smbcommerce_ai_provider].model}),". Choosing a model yourself keeps your cost predictable when defaults change."]})}),e("table",{className:"widefat striped",style:{marginTop:8},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Provider"}),e("th",{children:"Model used"}),e("th",{children:"Estimated cost per conversation"})]})}),e("tbody",{children:["anthropic","openai","gemini"].map(n=>{const c=W[n];return c?e("tr",{children:[e("td",{children:[n==="anthropic"?"Claude":n==="openai"?"OpenAI":"Gemini",t.smbcommerce_ai_provider===n?" (main)":""]}),e("td",{children:[e("code",{children:c.model})," ",c.source==="default"?"(default)":c.source==="replacement"?"(replacement)":""]}),e("td",{children:c.estimate_per_conversation===null?"No price on file":`about $${Number(c.estimate_per_conversation).toFixed(4)}`})]},n):null})})]}),e("p",{className:"description",children:"Estimate for a typical conversation: 2 replies with about 7,000 input and 80 output tokens each, at the provider's list price. Your real cost is recorded per request on the AI Value & Performance tab."})]})]}),A&&A.managed&&e("div",{className:"smb-commerce-card",style:{borderLeft:`4px solid ${A.is_paid?"#1b7a51":"#9a5c06"}`},children:[e("h3",{children:[e("span",{className:"dashicons dashicons-awards"})," Licence"]}),e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:200},children:"Current plan"}),e("td",{children:e("strong",{children:A.label})})]}),e("tr",{children:[e("td",{children:"Valid until"}),e("td",{children:A.is_lifetime?"Lifetime, never expires":A.expires_at||"—"})]})]})}),!A.is_paid&&e("p",{className:"description",style:{marginBottom:10},children:"These features need a paid plan: business analytics, AI budget caps, webhooks, lead scoring, provider failover, response cache, ungrounded-figure blocking and product variation tools. Chat, product sync, the sales playbook and lead capture keep working as normal."}),A.tier==="starter"&&e("p",{className:"description",style:{marginBottom:10},children:"The Pro plan adds provider failover, AI budget caps, coupon validation, lead scoring, advanced analytics, price verification and outgoing webhooks."}),A.upgrade_url&&A.tier!=="pro"&&e("a",{className:"button button-primary",href:A.upgrade_url,children:"View pricing"}),A.account_url&&e("a",{className:"button",style:{marginLeft:8},href:A.account_url,children:"Manage licence"})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-chart-bar"})," Business performance"]}),e("p",{className:"description",style:{marginTop:0},children:["Last ",H," days."," ",[7,30,90].map(n=>e("button",{type:"button",className:"button button-small",style:{marginRight:4,fontWeight:H===n?700:400},onClick:()=>U(n),children:[n," days"]},n))]}),h&&h.locked?e("p",{className:"description",children:"A paid plan is required to view analytics. Data is still being recorded — upgrade and your full history appears immediately."}):h&&h.summary?e("div",{children:[e("table",{className:"widefat striped",style:{marginBottom:14},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:260},children:"Conversations"}),e("td",{children:e("strong",{children:h.summary.conversations})})]}),e("tr",{children:[e("td",{children:"Leads captured"}),e("td",{children:[e("strong",{children:h.summary.leads})," (",h.summary.lead_rate,"% of conversations) · ",h.summary.hot_leads," hot"]})]}),e("tr",{children:[e("td",{children:"Not handed off to staff"}),e("td",{children:[e("strong",{children:[h.summary.resolution_rate,"%"]})," · handed off ",h.summary.handoff_rate,"%"]})]}),e("tr",{children:[e("td",{children:"AI cost (estimated)"}),e("td",{children:e("strong",{children:J(h.summary.cost_usd)})})]}),e("tr",{children:[e("td",{children:"Cost per conversation"}),e("td",{children:J(h.summary.cost_per_conversation)})]}),e("tr",{children:[e("td",{children:"Cost per lead"}),e("td",{children:h.summary.leads>0?J(h.summary.cost_per_lead):"—"})]}),e("tr",{children:[e("td",{children:"AI calls · average latency"}),e("td",{children:[h.summary.ai_calls," calls · ",h.summary.avg_latency_ms,"ms · ",h.summary.error_rate,"% errors"]})]}),e("tr",{children:[e("td",{children:"Tokens used"}),e("td",{children:(h.summary.prompt_tokens+h.summary.completion_tokens).toLocaleString()})]})]})}),h.budget&&(h.budget.daily_limit>0||h.budget.monthly_limit>0)&&e("p",{className:"description",children:[h.budget.daily_limit>0&&e("span",{children:["Today: ",J(h.budget.daily_spent)," / ",J(h.budget.daily_limit)," (",h.budget.daily_percent,"%). "]}),h.budget.monthly_limit>0&&e("span",{children:["This month: ",J(h.budget.monthly_spent)," / ",J(h.budget.monthly_limit)," (",h.budget.monthly_percent,"%)."]})]}),h.cache&&h.cache.hits>0&&e("p",{className:"description",children:["Cache: ",h.cache.entries," entries stored, ",h.cache.saved_calls," model calls saved."]}),h.handoffs&&h.handoffs.length>0&&e("div",{style:{marginTop:14},children:[e("strong",{style:{fontSize:13},children:"Recently handed off to a human"}),e("p",{className:"description",style:{marginTop:2},children:"This is where the bot ran out of knowledge — worth reading to fill the gaps."}),e("table",{className:"widefat striped",children:e("tbody",{children:h.handoffs.map(n=>e("tr",{children:[e("td",{style:{width:150},children:n.updated_at}),e("td",{style:{width:70},children:n.score}),e("td",{children:n.intent||n.summary||"—"})]},n.id))})})]})]}):e("p",{className:"description",children:"No data yet. Figures appear after the first conversation."})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-shield"})," Cost & reliability"]}),!(v("budget")&&v("provider_failover")&&v("output_guard"))&&e("p",{className:"description",children:["AI budget caps, provider failover and price verification need the Pro plan.",!v("response_cache")&&" The response cache needs the Starter plan."]}),v("budget")&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI budget cap (USD)"}),e("div",{style:{display:"flex",gap:10,flexWrap:"wrap"},children:[e("input",{type:"number",min:"0",step:"0.5",style:{maxWidth:150},placeholder:"Per day",value:t.smbcommerce_budget_daily_usd,onChange:n=>s({...t,smbcommerce_budget_daily_usd:n.target.value})}),e("input",{type:"number",min:"0",step:"1",style:{maxWidth:150},placeholder:"Per month",value:t.smbcommerce_budget_monthly_usd,onChange:n=>s({...t,smbcommerce_budget_monthly_usd:n.target.value})})]}),e("p",{className:"description",children:["Set to 0 for no limit. When the cap is reached the bot stops replying and hands the conversation to your team; the visitor never sees anything about billing. Cost is an",e("strong",{children:" estimate"})," based on published rates, not your actual invoice."]})]}),v("provider_failover")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_provider_fallback==="1",onChange:n=>s({...t,smbcommerce_enable_provider_fallback:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Automatic provider failover"}),e("p",{className:"description",style:{margin:0},children:"If the primary provider fails or is rate limited, try another provider that already has an API key instead of showing the visitor an error."})]})]}),v("output_guard")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_block_ungrounded_prices==="1",onChange:n=>s({...t,smbcommerce_block_ungrounded_prices:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Block replies containing ungrounded figures"}),e("p",{className:"description",style:{margin:0},children:"Every price figure in a reply is checked against the real data given to the AI. When off, mismatches are still logged but not blocked — turn this on once the log shows few false positives."})]})]}),v("response_cache")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_response_cache==="1",onChange:n=>s({...t,smbcommerce_enable_response_cache:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Cache repeated questions"}),e("p",{className:"description",style:{margin:0},children:["Identical questions reuse the previous answer. Questions about price, stock, orders and coupons are never cached.",e("input",{type:"number",min:"1",style:{width:80,marginLeft:8},value:t.smbcommerce_cache_ttl_hours,onChange:n=>s({...t,smbcommerce_cache_ttl_hours:n.target.value})})," hours",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:Me,children:"Clear cache"})]})]})]})]}),g&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," Outgoing webhooks"]}),v("webhooks")?e(z,{children:[e("p",{className:"description",style:{marginTop:0},children:["Send events to n8n, Make, Zapier or your CRM. Events:"," ",e("code",{children:"lead.created"})," ",e("code",{children:"lead.qualified"})," ",e("code",{children:"human.requested"})," ",e("code",{children:"budget.exceeded"})," ",e("code",{children:"ai.failed"})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Webhook URL"}),e("input",{type:"url",placeholder:"https://n8n.example.com/webhook/...",value:t.smbcommerce_webhook_url,onChange:n=>s({...t,smbcommerce_webhook_url:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Signing secret"}),e("input",{type:"password",placeholder:t.smbcommerce_webhook_secret_exists?"•••••••••• (saved)":"Any secret string",value:t.smbcommerce_webhook_secret,onChange:n=>s({...t,smbcommerce_webhook_secret:n.target.value})}),e("p",{className:"description",children:["With a secret set, every request carries the header ",e("code",{children:"X-SMB-Commerce-Signature: sha256=..."}),", computed with HMAC-SHA256 over the body. The receiver verifies it to confirm the request came from this site."]})]}),e("button",{type:"button",className:"button",onClick:ee,children:"Send test"}),O&&e("p",{className:"description",style:{marginTop:8},children:O.pending?"Sending...":O.success?`Sent. The receiver returned HTTP ${O.status}${O.signed?", signed":", unsigned"}.`:`Error: ${O.message||"unknown error"}`})]}):e("p",{className:"description",style:{marginTop:0},children:"Outgoing webhooks are available in the Pro plan."})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-megaphone"})," Sales playbook"]}),e("p",{className:"description",style:{marginTop:0},children:["This is what decides whether the bot actually sells. Available variables:",e("code",{children:"{brand}"})," ",e("code",{children:"{hotline}"})," ",e("code",{children:"{zalo}"})," ",e("code",{children:"{messenger}"}),"."]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Main playbook"}),e("textarea",{rows:16,style:{fontFamily:"monospace",fontSize:"12px"},value:t.smbcommerce_sales_playbook,onChange:n=>s({...t,smbcommerce_sales_playbook:n.target.value})}),e("p",{className:"description",children:["Role, tone, sales flow, objection handling and how to ask for contact details.",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:ze,children:"Restore default playbook"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Business information"}),e("textarea",{rows:6,style:{fontFamily:"monospace",fontSize:"12px"},placeholder:"Business hours, payment, delivery, returns...",value:t.smbcommerce_business_info,onChange:n=>s({...t,smbcommerce_business_info:n.target.value})}),k!==null&&t.smbcommerce_business_info===k&&e("div",{className:"notice notice-warning inline",children:e("p",{children:"This is the sample text added by an earlier version, not your store's details. The assistant does not use it. Replace it with your real opening hours, payment, delivery and return terms, then click Save."})}),e("p",{className:"description",children:"The bot may answer policy questions only from this text. Leave it empty and the bot hands off to your team instead of guessing."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:["Detailed policies (",e("code",{children:"get_policy"}),")"]}),e("p",{className:"description",style:{marginTop:0},children:"Any field left empty falls back to the Business information above for that policy type."}),[["smbcommerce_refund_policy","Refunds"],["smbcommerce_warranty_policy","Warranty"],["smbcommerce_delivery_policy","Delivery"],["smbcommerce_privacy_policy","Privacy"]].map(([n,c])=>e("div",{style:{marginBottom:10},children:[e("label",{style:{fontWeight:500},children:c}),e("textarea",{rows:3,value:t[n],onChange:L=>s({...t,[n]:L.target.value})})]},n))]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_adaptive_playbook==="1",onChange:n=>s({...t,smbcommerce_enable_adaptive_playbook:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Situational guidance"}),e("p",{className:"description",style:{margin:0},children:"Detects whether the visitor is asking about price, pushing back on cost, hesitating, comparing or ready to buy, then adds guidance for that specific turn. Also reminds the bot to ask for contact details after a few exchanges."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-products"})," Product data"]}),e("p",{className:"description",style:{marginTop:0},children:"The bot can only recommend products that have been synced into its cache. Without a sync it has nothing to sell."}),C&&e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:220},children:"WooCommerce"}),e("td",{children:C.woocommerce_active?"Active":"NOT ACTIVE"})]}),e("tr",{children:[e("td",{children:"Products in cache"}),e("td",{children:e("strong",{children:C.total_cached})})]}),e("tr",{children:[e("td",{children:"Last sync"}),e("td",{children:C.last_sync||"Never"})]}),e("tr",{children:[e("td",{children:"Next scheduled run"}),e("td",{children:C.next_scheduled_sync||"Not scheduled"})]})]})}),e("button",{type:"button",className:"button",onClick:Ue,disabled:$,children:$?"Syncing...":"Sync products now"}),I&&e("p",{className:"description",style:{marginTop:8},children:I.success?`Synced ${I.synced} products. Total in cache: ${I.total_cached}.`:`Error: ${I.message||I.error||"unknown error"}`})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-tools"})," Advanced features"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_link_reading==="1",onChange:n=>s({...t,smbcommerce_enable_link_reading:n.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Let the bot read links visitors send"}),e("p",{className:"description",style:{margin:0},children:["When enabled, every URL a visitor pastes into the chat is sent to the external service ",e("code",{children:"r.jina.ai"})," to fetch its content. Leave this off if visitors might paste private or internal links. Off by default."]})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Maximum upload size (MB)"}),e("input",{type:"number",value:t.smbcommerce_max_upload_size,onChange:n=>s({...t,smbcommerce_max_upload_size:n.target.value}),min:"1",max:"50"}),e("p",{className:"description",children:"5MB is recommended to avoid filling up server storage."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"File retention (days)"}),e("input",{type:"number",value:t.smbcommerce_media_retention_days,onChange:n=>s({...t,smbcommerce_media_retention_days:n.target.value}),min:"1",max:"365"}),e("p",{className:"description",children:'Files (images, documents) in CLOSED conversations older than this are deleted automatically to keep the server clean. Their links are replaced with an "Expired" notice.'})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"System guardrails prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"e.g. You are a customer support AI. Never disclose sensitive information...",value:t.smbcommerce_system_guardrails_prompt,onChange:n=>s({...t,smbcommerce_system_guardrails_prompt:n.target.value})}),e("p",{className:"description",children:"Highest-priority guardrail, always pinned at the top of the context to resist jailbreaks and hallucination."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Sales & CSKH Flow Prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"Enter your sales playbook here...",value:t.smbcommerce_meta_prompt_template,onChange:n=>s({...t,smbcommerce_meta_prompt_template:n.target.value})})]}),v("telegram")?e(z,{children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram bot token"}),e("input",{type:"password",placeholder:a.telegram?"•••••••••••• (saved)":"Enter the bot token from BotFather...",value:t.smbcommerce_telegram_bot_token,onChange:n=>s({...t,smbcommerce_telegram_bot_token:n.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram Chat ID"}),e("input",{type:"text",placeholder:"e.g. -1001234567890",value:t.smbcommerce_telegram_chat_id,onChange:n=>s({...t,smbcommerce_telegram_chat_id:n.target.value})}),e("p",{className:"description",children:"Without a Chat ID new-lead notifications cannot be delivered, even with a Bot Token set."})]})]}):e("p",{className:"description",id:"smb-commerce-telegram-plan-note",children:"Telegram notifications are available in the Starter plan."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Handoff Telegram Webhook URL"}),e("input",{type:"text",placeholder:"https://...",value:t.smbcommerce_handoff_webhook,onChange:n=>s({...t,smbcommerce_handoff_webhook:n.target.value})}),e("p",{className:"description",children:"URL that receives an urgent notification whenever a handoff is triggered."})]})]}),e("div",{className:"smb-commerce-actions",children:[e("button",{type:"submit",className:"button button-primary button-large",disabled:d,children:d?"Saving...":"Save all settings"}),e("button",{type:"button",className:"button button-secondary",onClick:Ve,disabled:y,style:{marginLeft:"10px"},children:y?"Testing...":"Test AI connection"})]}),_&&e("div",{style:{marginTop:"16px",padding:"14px 18px",borderRadius:"8px",background:_.error?"#fff0f0":(pe=_.test_result)!=null&&pe.startsWith("OK")?"#f0fff4":"#fffbe6",border:`1px solid ${_.error?"#ffcdd2":(_e=_.test_result)!=null&&_e.startsWith("OK")?"#a7f3d0":"#fde68a"}`,fontSize:"13px",lineHeight:"1.7"},children:_.error?e("div",{children:[e("strong",{children:"Error:"})," ",_.error]}):e(z,{children:[e("div",{children:[e("strong",{children:"Selected provider:"})," ",_.configured_provider]}),e("div",{children:[e("strong",{children:"Class:"})," ",_.provider_class]}),e("div",{children:[e("strong",{children:"Gemini key:"})," ",_.has_gemini_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"OpenAI key:"})," ",_.has_openai_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"Claude key:"})," ",_.has_claude_key?"Saved":"Not set"]}),e("div",{style:{marginTop:"8px",padding:"8px",background:"#fff",borderRadius:"6px",border:"1px solid #e5e7eb"},children:[e("strong",{children:"Test result:"}),e("br",{}),e("code",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:_.test_result||"No provider"})]})]})})]})}),e("div",{className:"smb-commerce-settings-preview-col",children:e("div",{className:"preview-sticky",children:[e("h3",{children:"Live preview"}),e("p",{className:"description",children:"How the widget will look on your site."}),e("div",{className:"widget-preview-box",style:t.smbcommerce_font_family&&t.smbcommerce_font_family!=="system"?{fontFamily:`'${t.smbcommerce_font_family}', sans-serif`}:void 0,children:[e("div",{className:"widget-preview-header",style:{backgroundColor:t.smbcommerce_primary_color},children:[e("div",{className:"widget-preview-header-brand",children:[t.smbcommerce_logo_url?e("img",{src:t.smbcommerce_logo_url,alt:"Logo",className:"widget-preview-logo"}):e("svg",{viewBox:"0 0 24 24",width:"24",height:"24",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),e("span",{children:t.smbcommerce_brand_name||"Brand name"})]}),e("div",{className:"widget-preview-close",children:"×"})]}),e("div",{className:"widget-preview-body",children:[e("div",{className:"preview-message bot",children:"Hello! How can I help you today?"}),e("div",{className:"preview-message user",style:{backgroundColor:t.smbcommerce_primary_color,color:"#fff"},children:"I'd like to ask about your services."})]}),e("div",{className:"widget-preview-footer",children:[e("button",{style:{background:"transparent",border:"none",color:"#666",padding:"0 8px"},children:e("svg",{viewBox:"0 0 24 24",width:"20",height:"20",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"})})}),e("input",{type:"text",placeholder:"Type a message...",disabled:!0}),e("button",{style:{backgroundColor:t.smbcommerce_primary_color},children:e("svg",{viewBox:"0 0 24 24",width:"16",height:"16",stroke:"currentColor",strokeWidth:"2",fill:"none",children:[e("line",{x1:"22",y1:"2",x2:"11",y2:"13"}),e("polygon",{points:"22 2 15 22 11 13 2 9 22 2"})]})})]})]})]})})]})]})}var $e;const _t=(($e=window.SMBCommerceAdmin)==null?void 0:$e.i18n)||{};function l(r,...t){let s=0;return(_t[r]||r).replace(/%(\d+)\$s/g,(a,o)=>String(t[Number(o)-1]??"")).replace(/%s/g,()=>String(t[s++]??"")).replace(/%%/g,"%")}const F=r=>{const t=Number(r)||0,s=t<0?"−":"",a=Math.abs(t);return`${s}$${a.toFixed(a>0&&a<1?4:2)}`},bt=(r,t)=>{try{return new Intl.NumberFormat(void 0,{style:"currency",currency:t}).format(r)}catch{return`${r.toFixed(2)} ${t}`}},gt=r=>r?new Date(r.replace(" ","T")+"Z").toLocaleDateString():"",We={24:"hours24",72:"hours72",168:"days7"},ae={processing:"statusProcessing",completed:"statusCompleted","on-hold":"statusOnHold"},oe={ai_conversation:"unitAiConversation",resolved_conversation:"unitResolved",month:"unitMonth"},ft=[["ai","sourceAi"],["rule","sourceRule"],["customer","sourceCustomer"],["agent","sourceAgent"],["budget","sourceBudget"]];function yt(){const{notices:r,push:t,dismiss:s}=ce(),[a,o]=p(30),[d,i]=p(null),[f,b]=p(!0),[_,E]=p(!1),[y,w]=p(null),[$,B]=p(!1),I=async u=>{b(!0),E(!1);try{const h=await N.get(`/admin/value-metrics?days=${u}`);i(h),w(S=>S??h.settings)}catch{E(!0)}finally{b(!1)}};j(()=>{I(a)},[a]);const D=async u=>{if(u.preventDefault(),!!y){B(!0);try{const h=await N.post("/admin/value-settings",y);w(h),t("success",l("saved")),I(a)}catch{t("error",l("saveError"))}finally{B(!1)}}},C=(u,h)=>w(S=>S&&{...S,reference:{...S.reference,[u]:h}});return e("section",{className:"smb-value","aria-labelledby":"smb-value-title",children:[e(ie,{notices:r,onDismiss:s}),e("div",{className:"smb-value-header",children:[e("div",{children:[e("h2",{id:"smb-value-title",children:l("tab")}),e("p",{className:"smb-value-intro",children:l("intro")}),d&&e("p",{className:"smb-value-meta",children:d.measurement_started_at?l("measuredSince",gt(d.measurement_started_at)):l("notMeasured")})]}),e("label",{className:"smb-value-period",children:[e("span",{children:l("period")}),e("select",{value:a,onChange:u=>o(Number(u.target.value)),children:[7,30,90].map(u=>e("option",{value:u,children:l("lastDays",u)},u))})]})]}),f&&e("p",{role:"status",className:"smb-value-status",children:l("loading")}),_&&e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:l("loadError")})}),d&&!_&&e("div",{className:"smb-value-grid","aria-busy":f,children:[e(vt,{cost:d.cost}),e(wt,{orders:d.orders,allowed:d.revenue_allowed}),e(kt,{outcomes:d.outcomes,sources:d.handoffs_by_source})]}),y&&e("details",{className:"smb-value-settings",children:[e("summary",{children:l("settingsTitle")}),e("form",{onSubmit:D,children:[e("fieldset",{children:[e("legend",{children:l("costTitle")}),e("p",{className:"description",children:l("refHelp")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[l("refLabel"),e("input",{type:"text",maxLength:80,value:y.reference.label,onChange:u=>C("label",u.target.value)})]}),e("label",{children:[l("refUnit"),e("select",{value:y.reference.unit,onChange:u=>C("unit",u.target.value),children:Object.keys(oe).map(u=>e("option",{value:u,children:l(oe[u])},u))})]}),e("label",{children:[l("refPrice"),e("input",{type:"number",min:"0",step:"0.0001",value:y.reference.price,onChange:u=>C("price",Number(u.target.value))})]}),e("label",{children:[l("refSource"),e("input",{type:"text",maxLength:200,value:y.reference.source,onChange:u=>C("source",u.target.value)})]}),e("label",{children:[l("refDate"),e("input",{type:"date",value:y.reference.date,onChange:u=>C("date",u.target.value)})]}),e("label",{children:[l("pluginMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:y.plugin_cost_monthly,onChange:u=>w({...y,plugin_cost_monthly:Number(u.target.value)})})]}),e("label",{children:[l("otherMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:y.other_cost_monthly,onChange:u=>w({...y,other_cost_monthly:Number(u.target.value)})})]})]})]}),e("fieldset",{children:[e("legend",{children:l("ordersTitle")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[l("window"),e("select",{value:y.window_hours,onChange:u=>w({...y,window_hours:Number(u.target.value)}),"aria-describedby":"smb-window-help",children:[24,72,168].map(u=>e("option",{value:u,children:l(We[u])},u))}),e("span",{id:"smb-window-help",className:"description",children:l("windowHelp")})]}),e("div",{role:"group","aria-labelledby":"smb-statuses-label",children:[e("span",{id:"smb-statuses-label",className:"smb-value-group-label",children:l("statuses")}),Object.keys(ae).map(u=>e("label",{className:"smb-value-check",children:[e("input",{type:"checkbox",checked:y.statuses.includes(u),onChange:h=>w({...y,statuses:h.target.checked?[...y.statuses,u]:y.statuses.filter(S=>S!==u)})}),l(ae[u])]},u))]})]})]}),e("button",{type:"submit",className:"button button-primary",disabled:$,children:l($?"saving":"save")})]})]})]})}function vt({cost:r}){const t=r.reference;return e("article",{className:"smb-value-card","aria-labelledby":"smb-cost-title",children:[e("h3",{id:"smb-cost-title",children:l("costTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.ai_conversations)}),e("dl",{className:"smb-value-rows",children:[e("div",{children:[e("dt",{children:l("aiCost")}),e("dd",{children:[e("strong",{children:F(r.ai_cost_total)}),e("span",{className:"smb-value-sub",children:l("aiCostSplit",F(r.ai_cost_usage),F(r.ai_cost_estimated))}),r.unpriced_requests>0&&e("span",{className:"smb-value-sub",children:l("unpriced",r.unpriced_requests)})]})]}),e("div",{children:[e("dt",{children:l("pluginCost")}),e("dd",{children:F(r.plugin_cost)})]}),e("div",{children:[e("dt",{children:l("otherCost")}),e("dd",{children:F(r.other_cost)})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:l("totalCost")}),e("dd",{children:e("strong",{children:F(r.total_cost)})})]}),t&&e(z,{children:[e("div",{children:[e("dt",{children:l("referenceCost",t.label)}),e("dd",{children:[F(t.cost),e("span",{className:"smb-value-sub",children:[l("referenceVolume",t.volume,F(t.price))," ",l(oe[t.unit]||"unitAiConversation")]}),t.source&&e("span",{className:"smb-value-sub",children:[l("referenceSource",t.source),t.date?` · ${t.date}`:""]})]})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:l("difference")}),e("dd",{children:e("strong",{children:F(t.difference)})})]})]})]}),e("p",{className:"smb-value-note",children:l(t?"differenceNote":"noReference")}),e("p",{className:"smb-value-note",children:l("costScope")})]})}function wt({orders:r,allowed:t}){const s=r.revenue_by_currency||{},a=(r.statuses||[]).map(o=>l(ae[o]||o)).join(", ");return e("article",{className:"smb-value-card","aria-labelledby":"smb-orders-title",children:[e("h3",{id:"smb-orders-title",children:l("ordersTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.assisted_orders)}),e("p",{className:"smb-value-big",children:[e("strong",{children:r.assisted_orders})," ",e("span",{children:l("orders")})]}),e("p",{className:"smb-value-caption",children:l("assistedCaption")}),e("h4",{children:l("revenue")}),t?Object.keys(s).length>0?e("ul",{className:"smb-value-list",children:Object.entries(s).map(([o,d])=>e("li",{children:e("strong",{children:bt(d,o)})},o))}):e("p",{children:l("noRevenue")}):e("p",{children:l("revenueLocked")}),t&&e("p",{className:"smb-value-note",children:l("revenueMethod")}),e("p",{className:"smb-value-note",children:l("orderRule",l(We[r.window_hours]||"hours24"),a)})]})}function kt({outcomes:r,sources:t}){const s=[["confirmed",r.resolved_confirmed,r.counts.resolved_confirmed],["inferred",r.resolved_inferred,r.counts.resolved_inferred],["handedOff",r.handed_off,r.counts.handed_off],["otherOutcome",r.unresolved_or_unknown,r.counts.unresolved+r.counts.abandoned+r.counts.unknown]];return e("article",{className:"smb-value-card","aria-labelledby":"smb-outcomes-title",children:[e("h3",{id:"smb-outcomes-title",children:l("outcomesTitle")}),e("p",{className:"smb-value-sample",children:l("sample",r.finished)}),r.insufficient_data?e("p",{className:"smb-value-empty",children:l("notEnough",r.finished,30)}):e("dl",{className:"smb-value-rows",children:s.map(([a,o,d])=>e("div",{children:[e("dt",{children:l(a)}),e("dd",{children:[e("strong",{children:o?`${o.rate}%`:"—"})," ",e("span",{className:"smb-value-sub-inline",children:["(",d,")"]}),(o==null?void 0:o.ci95)&&e("span",{className:"smb-value-sub",children:l("ci",o.ci95.low,o.ci95.high)})]})]},a))}),e("p",{className:"smb-value-note",children:l("outcomeNote")}),e("h4",{children:l("bySource")}),e("ul",{className:"smb-value-list smb-value-sources",children:ft.map(([a,o])=>e("li",{children:[e("span",{children:l(o)})," ",e("strong",{children:(t==null?void 0:t[a])??0})]},a))})]})}const Nt=["value","inbox","rules","leads"],He="smbcommerce_admin_tab";function xt(){try{const r=window.localStorage.getItem(He);return r&&Nt.includes(r)?r:"value"}catch{return"value"}}const Ct=()=>{var o,d;const[r,t]=p(xt),s=i=>{t(i);try{window.localStorage.setItem(He,i)}catch{}},a=((d=(o=window.SMBCommerceAdmin)==null?void 0:o.i18n)==null?void 0:d.tab)||"AI Value & Performance";return e("div",{className:"smb-commerce-admin-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Admin"}),e("h2",{className:"nav-tab-wrapper",children:[e("button",{className:`nav-tab ${r==="value"?"nav-tab-active":""}`,onClick:()=>s("value"),"aria-current":r==="value"?"page":void 0,children:a}),e("button",{className:`nav-tab ${r==="inbox"?"nav-tab-active":""}`,onClick:()=>s("inbox"),"aria-current":r==="inbox"?"page":void 0,children:"Inbox"}),e("button",{className:`nav-tab ${r==="rules"?"nav-tab-active":""}`,onClick:()=>s("rules"),"aria-current":r==="rules"?"page":void 0,children:"Rules Engine"}),e("button",{className:`nav-tab ${r==="leads"?"nav-tab-active":""}`,onClick:()=>s("leads"),"aria-current":r==="leads"?"page":void 0,children:"Leads"})]}),e("div",{style:{marginTop:20},children:[r==="value"&&e(yt,{}),r==="inbox"&&e(ct,{}),r==="rules"&&e(ht,{}),r==="leads"&&e(ut,{})]})]})},St=()=>e("div",{className:"smb-commerce-settings-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Settings"}),e(pt,{})]}),Ae=document.getElementById("smb-commerce-admin-root");Ae&&Be(Ae).render(e(Ct,{}));const Le=document.getElementById("smb-commerce-settings-root");Le&&Be(Le).render(e(St,{}));
     4What is your return policy?`,value:t.smbcommerce_suggestion_chips,onChange:s=>n({...t,smbcommerce_suggestion_chips:s.target.value})}),e("p",{className:"description",children:"One suggestion per line. Visitors can tap one to send that question instantly."})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-network"})," AI configuration"]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Assistant mode"}),e("select",{value:t.smbcommerce_runtime_mode,onChange:s=>n({...t,smbcommerce_runtime_mode:s.target.value}),children:[e("option",{value:"smart_hybrid",children:"Smart hybrid — answer locally where possible, use AI when needed (recommended)"}),e("option",{value:"local_only",children:"Local only — never send conversations to an AI provider"}),e("option",{value:"ai_enhanced",children:"AI enhanced — let the AI lead every reply"})]}),e("p",{className:"description",children:"Local only answers greetings, policy questions, cart and order lookups from your own store data and costs no tokens. It cannot answer anything else — those customers are asked to contact you directly. This setting covers AI providers only; Telegram notifications, webhooks and licensing still reach the internet."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-reply-language",children:"Reply language"}),e("select",{id:"smb-reply-language",value:t.smbcommerce_reply_language,onChange:s=>n({...t,smbcommerce_reply_language:s.target.value}),children:[e("option",{value:"customer",children:"The customer's language (recommended)"}),e("option",{value:"store",children:"As in earlier versions: follow the sales playbook and site language"})]}),e("p",{className:"description",children:"With the customer's language, the assistant replies in the language each customer writes in and keeps product names, prices and order numbers exactly as they are in your store. Fixed messages, such as the handoff confirmation, follow the customer's language when this site has a translation for it."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI provider"}),e("select",{value:t.smbcommerce_ai_provider,onChange:s=>n({...t,smbcommerce_ai_provider:s.target.value}),children:[e("option",{value:"openai",children:"OpenAI (GPT-4o-mini)"}),e("option",{value:"anthropic",children:"Anthropic (Claude 3.5)"}),e("option",{value:"gemini",children:"Google Gemini"}),(K||t.smbcommerce_ai_provider==="wordpress")&&e("option",{value:"wordpress",children:"WordPress AI connection (Settings → Connectors)"})]}),t.smbcommerce_ai_provider==="wordpress"&&e("p",{className:"description",children:K?"Uses the AI provider you set up in Settings → Connectors. The key stays in WordPress; this plugin never reads it. The keys below are not needed for this option.":"No AI provider is set up in Settings → Connectors any more. Set one up there, or choose another provider here."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI API Key"}),e("input",{type:"password",placeholder:p.openai?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_openai_key,onChange:s=>n({...t,smbcommerce_openai_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Anthropic API Key"}),e("input",{type:"password",placeholder:p.anthropic?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_anthropic_key,onChange:s=>n({...t,smbcommerce_anthropic_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini API Key"}),e("input",{type:"password",placeholder:p.gemini?"•••••••••••• (saved)":"Enter API key...",value:t.smbcommerce_gemini_key,onChange:s=>n({...t,smbcommerce_gemini_key:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Gemini model"}),e("input",{type:"text",placeholder:"Leave empty for gemini-flash-latest",value:t.smbcommerce_gemini_model||"",onChange:s=>n({...t,smbcommerce_gemini_model:s.target.value})}),e("p",{className:"description",children:["Leave empty to always use Google's current Flash model. Enter a specific model (for example ",e("code",{children:"gemini-3.5-flash"}),") only if you need it."]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Claude Model"}),e("input",{type:"text",placeholder:"Leave empty for claude-sonnet-5-5",value:t.smbcommerce_anthropic_model,onChange:s=>n({...t,smbcommerce_anthropic_model:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"OpenAI Model"}),e("input",{type:"text",placeholder:"Leave empty for gpt-4o-mini",value:t.smbcommerce_openai_model,onChange:s=>n({...t,smbcommerce_openai_model:s.target.value})})]}),W&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Models in use"}),["anthropic","openai","gemini"].filter(s=>{var a;return(a=W[s])==null?void 0:a.retired}).map(s=>e("div",{className:"notice notice-warning inline",children:e("p",{children:["The saved ",s==="anthropic"?"Claude":s==="openai"?"OpenAI":"Gemini"," model ",e("code",{children:W[s].configured})," is no longer available from the provider. The assistant is using ",e("code",{children:W[s].model})," instead. Enter a current model above to choose for yourself."]})},s)),((he=W[t.smbcommerce_ai_provider])==null?void 0:he.source)==="default"&&e("div",{className:"notice notice-info inline",children:e("p",{children:["No model is chosen for your main provider, so the assistant uses the default ",e("code",{children:W[t.smbcommerce_ai_provider].model}),". Choosing a model yourself keeps your cost predictable when defaults change."]})}),e("table",{className:"widefat striped",style:{marginTop:8},children:[e("thead",{children:e("tr",{children:[e("th",{children:"Provider"}),e("th",{children:"Model used"}),e("th",{children:"Estimated cost per conversation"})]})}),e("tbody",{children:["anthropic","openai","gemini"].map(s=>{const a=W[s];return a?e("tr",{children:[e("td",{children:[s==="anthropic"?"Claude":s==="openai"?"OpenAI":"Gemini",t.smbcommerce_ai_provider===s?" (main)":""]}),e("td",{children:[e("code",{children:a.model})," ",a.source==="default"?"(default)":a.source==="replacement"?"(replacement)":""]}),e("td",{children:a.estimate_per_conversation===null?"No price on file":`about $${Number(a.estimate_per_conversation).toFixed(4)}`})]},s):null})})]}),e("p",{className:"description",children:"Estimate for a typical conversation: 2 replies with about 7,000 input and 80 output tokens each, at the provider's list price. Your real cost is recorded per request on the AI Value & Performance tab."})]})]}),S&&S.managed&&e("div",{className:"smb-commerce-card",style:{borderLeft:`4px solid ${S.is_paid?"#1b7a51":"#9a5c06"}`},children:[e("h3",{children:[e("span",{className:"dashicons dashicons-awards"})," Licence"]}),e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:200},children:"Current plan"}),e("td",{children:e("strong",{children:S.label})})]}),e("tr",{children:[e("td",{children:"Valid until"}),e("td",{children:S.is_lifetime?"Lifetime, never expires":S.expires_at||"—"})]})]})}),!S.is_paid&&e("p",{className:"description",style:{marginBottom:10},children:"These features need a paid plan: business analytics, AI budget caps, webhooks, lead scoring, provider failover, response cache, ungrounded-figure blocking and product variation tools. Chat, product sync, the sales playbook and lead capture keep working as normal."}),S.tier==="starter"&&e("p",{className:"description",style:{marginBottom:10},children:"The Pro plan adds provider failover, AI budget caps, coupon validation, lead scoring, advanced analytics, price verification and outgoing webhooks."}),S.upgrade_url&&S.tier!=="pro"&&e("a",{className:"button button-primary",href:S.upgrade_url,children:"View pricing"}),S.account_url&&e("a",{className:"button",style:{marginLeft:8},href:S.account_url,children:"Manage licence"})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-chart-bar"})," Business performance"]}),e("p",{className:"description",style:{marginTop:0},children:["Last ",B," days."," ",[7,30,90].map(s=>e("button",{type:"button",className:"button button-small",style:{marginRight:4,fontWeight:B===s?700:400},onClick:()=>P(s),children:[s," days"]},s))]}),l&&l.locked?e("p",{className:"description",children:"A paid plan is required to view analytics. Data is still being recorded — upgrade and your full history appears immediately."}):l&&l.summary?e("div",{children:[e("table",{className:"widefat striped",style:{marginBottom:14},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:260},children:"Conversations"}),e("td",{children:e("strong",{children:l.summary.conversations})})]}),e("tr",{children:[e("td",{children:"Leads captured"}),e("td",{children:[e("strong",{children:l.summary.leads})," (",l.summary.lead_rate,"% of conversations) · ",l.summary.hot_leads," hot"]})]}),e("tr",{children:[e("td",{children:"Not handed off to staff"}),e("td",{children:[e("strong",{children:[l.summary.resolution_rate,"%"]})," · handed off ",l.summary.handoff_rate,"%"]})]}),e("tr",{children:[e("td",{children:"AI cost (estimated)"}),e("td",{children:e("strong",{children:V(l.summary.cost_usd)})})]}),e("tr",{children:[e("td",{children:"Cost per conversation"}),e("td",{children:V(l.summary.cost_per_conversation)})]}),e("tr",{children:[e("td",{children:"Cost per lead"}),e("td",{children:l.summary.leads>0?V(l.summary.cost_per_lead):"—"})]}),e("tr",{children:[e("td",{children:"AI calls · average latency"}),e("td",{children:[l.summary.ai_calls," calls · ",l.summary.avg_latency_ms,"ms · ",l.summary.error_rate,"% errors"]})]}),e("tr",{children:[e("td",{children:"Tokens used"}),e("td",{children:(l.summary.prompt_tokens+l.summary.completion_tokens).toLocaleString()})]})]})}),l.budget&&(l.budget.daily_limit>0||l.budget.monthly_limit>0)&&e("p",{className:"description",children:[l.budget.daily_limit>0&&e("span",{children:["Today: ",V(l.budget.daily_spent)," / ",V(l.budget.daily_limit)," (",l.budget.daily_percent,"%). "]}),l.budget.monthly_limit>0&&e("span",{children:["This month: ",V(l.budget.monthly_spent)," / ",V(l.budget.monthly_limit)," (",l.budget.monthly_percent,"%)."]})]}),l.cache&&l.cache.hits>0&&e("p",{className:"description",children:["Cache: ",l.cache.entries," entries stored, ",l.cache.saved_calls," model calls saved."]}),l.handoffs&&l.handoffs.length>0&&e("div",{style:{marginTop:14},children:[e("strong",{style:{fontSize:13},children:"Recently handed off to a human"}),e("p",{className:"description",style:{marginTop:2},children:"This is where the bot ran out of knowledge — worth reading to fill the gaps."}),e("table",{className:"widefat striped",children:e("tbody",{children:l.handoffs.map(s=>e("tr",{children:[e("td",{style:{width:150},children:s.updated_at}),e("td",{style:{width:70},children:s.score}),e("td",{children:s.intent||s.summary||"—"})]},s.id))})})]})]}):e("p",{className:"description",children:"No data yet. Figures appear after the first conversation."})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-shield"})," Cost & reliability"]}),!(y("budget")&&y("provider_failover")&&y("output_guard"))&&e("p",{className:"description",children:["AI budget caps, provider failover and price verification need the Pro plan.",!y("response_cache")&&" The response cache needs the Starter plan."]}),y("budget")&&e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"AI budget cap (USD)"}),e("div",{style:{display:"flex",gap:10,flexWrap:"wrap"},children:[e("input",{type:"number",min:"0",step:"0.5",style:{maxWidth:150},placeholder:"Per day",value:t.smbcommerce_budget_daily_usd,onChange:s=>n({...t,smbcommerce_budget_daily_usd:s.target.value})}),e("input",{type:"number",min:"0",step:"1",style:{maxWidth:150},placeholder:"Per month",value:t.smbcommerce_budget_monthly_usd,onChange:s=>n({...t,smbcommerce_budget_monthly_usd:s.target.value})})]}),e("p",{className:"description",children:["Set to 0 for no limit. When the cap is reached the bot stops replying and hands the conversation to your team; the visitor never sees anything about billing. Cost is an",e("strong",{children:" estimate"})," based on published rates, not your actual invoice."]})]}),y("provider_failover")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_provider_fallback==="1",onChange:s=>n({...t,smbcommerce_enable_provider_fallback:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Automatic provider failover"}),e("p",{className:"description",style:{margin:0},children:"If the primary provider fails or is rate limited, try another provider that already has an API key instead of showing the visitor an error."})]})]}),y("output_guard")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_block_ungrounded_prices==="1",onChange:s=>n({...t,smbcommerce_block_ungrounded_prices:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Block replies containing ungrounded figures"}),e("p",{className:"description",style:{margin:0},children:"Every price figure in a reply is checked against the real data given to the AI. When off, mismatches are still logged but not blocked — turn this on once the log shows few false positives."})]})]}),y("response_cache")&&e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_response_cache==="1",onChange:s=>n({...t,smbcommerce_enable_response_cache:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Cache repeated questions"}),e("p",{className:"description",style:{margin:0},children:["Identical questions reuse the previous answer. Questions about price, stock, orders and coupons are never cached.",e("input",{type:"number",min:"1",style:{width:80,marginLeft:8},value:t.smbcommerce_cache_ttl_hours,onChange:s=>n({...t,smbcommerce_cache_ttl_hours:s.target.value})})," hours",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:X,children:"Clear cache"})]})]})]})]}),b&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," Outgoing webhooks"]}),y("webhooks")?e(D,{children:[e("p",{className:"description",style:{marginTop:0},children:["Send events to n8n, Make, Zapier or your CRM. Events:"," ",e("code",{children:"lead.created"})," ",e("code",{children:"lead.qualified"})," ",e("code",{children:"human.requested"})," ",e("code",{children:"budget.exceeded"})," ",e("code",{children:"ai.failed"})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Webhook URL"}),e("input",{type:"url",placeholder:"https://n8n.example.com/webhook/...",value:t.smbcommerce_webhook_url,onChange:s=>n({...t,smbcommerce_webhook_url:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Signing secret"}),e("input",{type:"password",placeholder:t.smbcommerce_webhook_secret_exists?"•••••••••• (saved)":"Any secret string",value:t.smbcommerce_webhook_secret,onChange:s=>n({...t,smbcommerce_webhook_secret:s.target.value})}),e("p",{className:"description",children:["With a secret set, every request carries the header ",e("code",{children:"X-SMB-Commerce-Signature: sha256=..."}),", computed with HMAC-SHA256 over the body. The receiver verifies it to confirm the request came from this site."]})]}),e("button",{type:"button",className:"button",onClick:z,children:"Send test"}),O&&e("p",{className:"description",style:{marginTop:8},children:O.pending?"Sending...":O.success?`Sent. The receiver returned HTTP ${O.status}${O.signed?", signed":", unsigned"}.`:`Error: ${O.message||"unknown error"}`})]}):e("p",{className:"description",style:{marginTop:0},children:"Outgoing webhooks are available in the Pro plan."})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-megaphone"})," Sales playbook"]}),e("p",{className:"description",style:{marginTop:0},children:["This is what decides whether the bot actually sells. Available variables:",e("code",{children:"{brand}"})," ",e("code",{children:"{hotline}"})," ",e("code",{children:"{zalo}"})," ",e("code",{children:"{messenger}"}),"."]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Main playbook"}),e("textarea",{rows:16,style:{fontFamily:"monospace",fontSize:"12px"},value:t.smbcommerce_sales_playbook,onChange:s=>n({...t,smbcommerce_sales_playbook:s.target.value})}),e("p",{className:"description",children:["Role, tone, sales flow, objection handling and how to ask for contact details.",e("button",{type:"button",className:"button button-small",style:{marginLeft:8},onClick:Te,children:"Restore default playbook"})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Business information"}),e("textarea",{rows:6,style:{fontFamily:"monospace",fontSize:"12px"},placeholder:"Business hours, payment, delivery, returns...",value:t.smbcommerce_business_info,onChange:s=>n({...t,smbcommerce_business_info:s.target.value})}),o!==null&&t.smbcommerce_business_info===o&&e("div",{className:"notice notice-warning inline",children:e("p",{children:"This is the sample text added by an earlier version, not your store's details. The assistant does not use it. Replace it with your real opening hours, payment, delivery and return terms, then click Save."})}),e("p",{className:"description",children:"The bot may answer policy questions only from this text. Leave it empty and the bot hands off to your team instead of guessing."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:["Detailed policies (",e("code",{children:"get_policy"}),")"]}),e("p",{className:"description",style:{marginTop:0},children:"Any field left empty falls back to the Business information above for that policy type."}),[["smbcommerce_refund_policy","Refunds"],["smbcommerce_warranty_policy","Warranty"],["smbcommerce_delivery_policy","Delivery"],["smbcommerce_privacy_policy","Privacy"]].map(([s,a])=>e("div",{style:{marginBottom:10},children:[e("label",{style:{fontWeight:500},children:a}),e("textarea",{rows:3,value:t[s],onChange:E=>n({...t,[s]:E.target.value})})]},s))]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_adaptive_playbook==="1",onChange:s=>n({...t,smbcommerce_enable_adaptive_playbook:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Situational guidance"}),e("p",{className:"description",style:{margin:0},children:"Detects whether the visitor is asking about price, pushing back on cost, hesitating, comparing or ready to buy, then adds guidance for that specific turn. Also reminds the bot to ask for contact details after a few exchanges."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-products"})," Product data"]}),e("p",{className:"description",style:{marginTop:0},children:"The bot can only recommend products that have been synced into its cache. Without a sync it has nothing to sell."}),v&&e("table",{className:"widefat striped",style:{marginBottom:12},children:e("tbody",{children:[e("tr",{children:[e("td",{style:{width:220},children:"WooCommerce"}),e("td",{children:v.woocommerce_active?"Active":"NOT ACTIVE"})]}),e("tr",{children:[e("td",{children:"Products in cache"}),e("td",{children:e("strong",{children:v.total_cached})})]}),e("tr",{children:[e("td",{children:"Last sync"}),e("td",{children:v.last_sync||"Never"})]}),e("tr",{children:[e("td",{children:"Next scheduled run"}),e("td",{children:v.next_scheduled_sync||"Not scheduled"})]})]})}),e("button",{type:"button",className:"button",onClick:Le,disabled:C,children:C?"Syncing...":"Sync products now"}),L&&e("p",{className:"description",style:{marginTop:8},children:L.success?`Synced ${L.synced} products. Total in cache: ${L.total_cached}.`:`Error: ${L.message||L.error||"unknown error"}`})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-cart"})," Cart in the chat"]}),e("p",{className:"description",style:{marginTop:0},children:"Product cards in the chat have an Add to cart button and the chat shows the customer's cart. Everything goes through your store's own cart, so prices, stock and coupon rules are checked by WooCommerce as usual. The assistant never changes a cart by itself."}),y("cart_actions_advanced")?e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-coupon-allowlist",children:"Discount codes the assistant may offer"}),e("textarea",{id:"smb-coupon-allowlist",rows:4,placeholder:`WELCOME10, 10
     5FREESHIP`,value:t.smbcommerce_coupon_allowlist,onChange:s=>n({...t,smbcommerce_coupon_allowlist:s.target.value})}),e("p",{className:"description",children:"One coupon code per line, up to 20. Add a comma and a number to cap the discount the assistant may offer: a percentage for percentage coupons, an amount for fixed coupons. The assistant offers a code only when a customer asks about discounts or hesitates over the price, and shows an Apply button. Codes that are not listed here are never offered. Leave it empty to turn this off."})]}):e("p",{className:"description",id:"smb-commerce-cart-plan-note",children:"Choosing product options on the card, changing quantities, removing items and applying discount codes in the chat are available in the Starter plan."})]}),se&&e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-rest-api"})," AI agents (Abilities API / MCP)"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_abilities_enabled==="1",onChange:s=>n({...t,smbcommerce_abilities_enabled:s.target.checked?"1":"0"})}),e("span",{className:"toggle-slider"})]}),e("div",{children:[e("strong",{children:"Let AI agents read your public catalog"}),e("p",{className:"description",style:{margin:0},children:"Adds three read-only abilities to WordPress: search products, product details and store policies. With the MCP Adapter plugin installed, AI assistants and agents can use them as tools. Only published, visible products and your policy texts are shared; never orders, carts or customer data. Limited to 60 requests per hour per visitor. Off by default."})]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-location"})," Order tracking"]}),e("p",{className:"description",style:{marginTop:0},children:"When a customer asks about an order, the chat shows an order card with its status and tracking number. Tracking numbers saved by WooCommerce Shipment Tracking or Advanced Shipment Tracking appear automatically. If your shipping plugin saves the tracking number in its own order field, enter that field's meta key below. Logged-in customers can ask about their orders without an order number."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-code-meta",children:"Tracking number meta key"}),e("input",{id:"smb-tracking-code-meta",type:"text",placeholder:"e.g. _ghn_order_code",value:t.smbcommerce_tracking_code_meta,onChange:s=>n({...t,smbcommerce_tracking_code_meta:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-carrier-meta",children:"Carrier meta key (optional)"}),e("input",{id:"smb-tracking-carrier-meta",type:"text",value:t.smbcommerce_tracking_carrier_meta,onChange:s=>n({...t,smbcommerce_tracking_carrier_meta:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-carrier",children:"Carrier name (optional)"}),e("input",{id:"smb-tracking-carrier",type:"text",placeholder:"e.g. GHN",value:t.smbcommerce_tracking_carrier,onChange:s=>n({...t,smbcommerce_tracking_carrier:s.target.value})}),e("p",{className:"description",children:"Used when there is no carrier meta key, or it is empty on an order."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{htmlFor:"smb-tracking-url",children:"Tracking link (optional)"}),e("input",{id:"smb-tracking-url",type:"url",placeholder:"https://carrier.example/track?code={code}",value:t.smbcommerce_tracking_url_template,onChange:s=>n({...t,smbcommerce_tracking_url_template:s.target.value})}),e("p",{className:"description",children:["A web address with ","{code}"," where the tracking number goes. The chat shows a Track button only when this is set. Meta keys may use letters, numbers, _ and -."]})]})]}),e("div",{className:"smb-commerce-card",children:[e("h3",{children:[e("span",{className:"dashicons dashicons-admin-tools"})," Advanced features"]}),e("div",{className:"smb-commerce-form-group toggle-group",children:[e("label",{className:"toggle-switch",children:[e("input",{type:"checkbox",checked:t.smbcommerce_enable_link_reading==="1",onChange:s=>n({...t,smbcommerce_enable_link_reading:s.target.checked?"1":"0"})}),e("span",{className:"slider"})]}),e("div",{children:[e("strong",{children:"Let the bot read links visitors send"}),e("p",{className:"description",style:{margin:0},children:["When enabled, every URL a visitor pastes into the chat is sent to the external service ",e("code",{children:"r.jina.ai"})," to fetch its content. Leave this off if visitors might paste private or internal links. Off by default."]})]})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Maximum upload size (MB)"}),e("input",{type:"number",value:t.smbcommerce_max_upload_size,onChange:s=>n({...t,smbcommerce_max_upload_size:s.target.value}),min:"1",max:"50"}),e("p",{className:"description",children:"5MB is recommended to avoid filling up server storage."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"File retention (days)"}),e("input",{type:"number",value:t.smbcommerce_media_retention_days,onChange:s=>n({...t,smbcommerce_media_retention_days:s.target.value}),min:"1",max:"365"}),e("p",{className:"description",children:'Files (images, documents) in CLOSED conversations older than this are deleted automatically to keep the server clean. Their links are replaced with an "Expired" notice.'})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"System guardrails prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"e.g. You are a customer support AI. Never disclose sensitive information...",value:t.smbcommerce_system_guardrails_prompt,onChange:s=>n({...t,smbcommerce_system_guardrails_prompt:s.target.value})}),e("p",{className:"description",children:"Highest-priority guardrail, always pinned at the top of the context to resist jailbreaks and hallucination."})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Sales & CSKH Flow Prompt"}),e("textarea",{style:{width:"100%",height:"120px",padding:"12px"},placeholder:"Enter your sales playbook here...",value:t.smbcommerce_meta_prompt_template,onChange:s=>n({...t,smbcommerce_meta_prompt_template:s.target.value})})]}),y("telegram")?e(D,{children:[e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram bot token"}),e("input",{type:"password",placeholder:p.telegram?"•••••••••••• (saved)":"Enter the bot token from BotFather...",value:t.smbcommerce_telegram_bot_token,onChange:s=>n({...t,smbcommerce_telegram_bot_token:s.target.value})})]}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Telegram Chat ID"}),e("input",{type:"text",placeholder:"e.g. -1001234567890",value:t.smbcommerce_telegram_chat_id,onChange:s=>n({...t,smbcommerce_telegram_chat_id:s.target.value})}),e("p",{className:"description",children:"Without a Chat ID new-lead notifications cannot be delivered, even with a Bot Token set."})]})]}):e("p",{className:"description",id:"smb-commerce-telegram-plan-note",children:"Telegram notifications are available in the Starter plan."}),e("div",{className:"smb-commerce-form-group",children:[e("label",{children:"Handoff Telegram Webhook URL"}),e("input",{type:"text",placeholder:"https://...",value:t.smbcommerce_handoff_webhook,onChange:s=>n({...t,smbcommerce_handoff_webhook:s.target.value})}),e("p",{className:"description",children:"URL that receives an urgent notification whenever a handoff is triggered."})]})]}),e("div",{className:"smb-commerce-actions",children:[e("button",{type:"submit",className:"button button-primary button-large",disabled:u,children:u?"Saving...":"Save all settings"}),e("button",{type:"button",className:"button button-secondary",onClick:Re,disabled:_,style:{marginLeft:"10px"},children:_?"Testing...":"Test AI connection"})]}),x&&e("div",{style:{marginTop:"16px",padding:"14px 18px",borderRadius:"8px",background:x.error?"#fff0f0":(ue=x.test_result)!=null&&ue.startsWith("OK")?"#f0fff4":"#fffbe6",border:`1px solid ${x.error?"#ffcdd2":(pe=x.test_result)!=null&&pe.startsWith("OK")?"#a7f3d0":"#fde68a"}`,fontSize:"13px",lineHeight:"1.7"},children:x.error?e("div",{children:[e("strong",{children:"Error:"})," ",x.error]}):e(D,{children:[e("div",{children:[e("strong",{children:"Selected provider:"})," ",x.configured_provider]}),e("div",{children:[e("strong",{children:"Class:"})," ",x.provider_class]}),e("div",{children:[e("strong",{children:"Gemini key:"})," ",x.has_gemini_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"OpenAI key:"})," ",x.has_openai_key?"Saved":"Not set"]}),e("div",{children:[e("strong",{children:"Claude key:"})," ",x.has_claude_key?"Saved":"Not set"]}),e("div",{style:{marginTop:"8px",padding:"8px",background:"#fff",borderRadius:"6px",border:"1px solid #e5e7eb"},children:[e("strong",{children:"Test result:"}),e("br",{}),e("code",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:x.test_result||"No provider"})]})]})})]})}),e("div",{className:"smb-commerce-settings-preview-col",children:e("div",{className:"preview-sticky",children:[e("h3",{children:"Live preview"}),e("p",{className:"description",children:"How the widget will look on your site."}),e("div",{className:"widget-preview-box",style:t.smbcommerce_font_family&&t.smbcommerce_font_family!=="system"?{fontFamily:`'${t.smbcommerce_font_family}', sans-serif`}:void 0,children:[e("div",{className:"widget-preview-header",style:{backgroundColor:t.smbcommerce_primary_color},children:[e("div",{className:"widget-preview-header-brand",children:[t.smbcommerce_logo_url?e("img",{src:t.smbcommerce_logo_url,alt:"Logo",className:"widget-preview-logo"}):e("svg",{viewBox:"0 0 24 24",width:"24",height:"24",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),e("span",{children:t.smbcommerce_brand_name||"Brand name"})]}),e("div",{className:"widget-preview-close",children:"×"})]}),e("div",{className:"widget-preview-body",children:[e("div",{className:"preview-message bot",children:"Hello! How can I help you today?"}),e("div",{className:"preview-message user",style:{backgroundColor:t.smbcommerce_primary_color,color:"#fff"},children:"I'd like to ask about your services."})]}),e("div",{className:"widget-preview-footer",children:[e("button",{style:{background:"transparent",border:"none",color:"#666",padding:"0 8px"},children:e("svg",{viewBox:"0 0 24 24",width:"20",height:"20",stroke:"currentColor",strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round",children:e("path",{d:"M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"})})}),e("input",{type:"text",placeholder:"Type a message...",disabled:!0}),e("button",{style:{backgroundColor:t.smbcommerce_primary_color},children:e("svg",{viewBox:"0 0 24 24",width:"16",height:"16",stroke:"currentColor",strokeWidth:"2",fill:"none",children:[e("line",{x1:"22",y1:"2",x2:"11",y2:"13"}),e("polygon",{points:"22 2 15 22 11 13 2 9 22 2"})]})})]})]})]})})]})]})}var we;const Ge=((we=window.SMBCommerceAdmin)==null?void 0:we.i18n)||{};function c(r,...t){let n=0;return(Ge[r]||r).replace(/%(\d+)\$s/g,(p,m)=>String(t[Number(m)-1]??"")).replace(/%s/g,()=>String(t[n++]??"")).replace(/%%/g,"%")}const j=r=>{const t=Number(r)||0,n=t<0?"−":"",p=Math.abs(t);return`${n}$${p.toFixed(p>0&&p<1?4:2)}`},Ve=(r,t)=>{try{return new Intl.NumberFormat(void 0,{style:"currency",currency:t}).format(r)}catch{return`${r.toFixed(2)} ${t}`}},Je=r=>r?new Date(r.replace(" ","T")+"Z").toLocaleDateString():"",Ce={24:"hours24",72:"hours72",168:"days7"},ne={processing:"statusProcessing",completed:"statusCompleted","on-hold":"statusOnHold"},ae={ai_conversation:"unitAiConversation",resolved_conversation:"unitResolved",month:"unitMonth"},Ke=[["ai","sourceAi"],["rule","sourceRule"],["customer","sourceCustomer"],["agent","sourceAgent"],["budget","sourceBudget"]];function Ye(){const{notices:r,push:t,dismiss:n}=oe(),[p,m]=d(30),[u,k]=d(null),[h,g]=d(!0),[x,T]=d(!1),[_,N]=d(null),[C,I]=d(!1),L=async i=>{g(!0),T(!1);try{const l=await w.get(`/admin/value-metrics?days=${i}`);k(l),N(R=>R??l.settings)}catch{T(!0)}finally{g(!1)}};F(()=>{L(p)},[p]);const $=async i=>{if(i.preventDefault(),!!_){I(!0);try{const l=await w.post("/admin/value-settings",_);N(l),t("success",c("saved")),L(p)}catch{t("error",c("saveError"))}finally{I(!1)}}},v=(i,l)=>N(R=>R&&{...R,reference:{...R.reference,[i]:l}});return e("section",{className:"smb-value","aria-labelledby":"smb-value-title",children:[e(ie,{notices:r,onDismiss:n}),e("div",{className:"smb-value-header",children:[e("div",{children:[e("h2",{id:"smb-value-title",children:c("tab")}),e("p",{className:"smb-value-intro",children:c("intro")}),u&&e("p",{className:"smb-value-meta",children:u.measurement_started_at?c("measuredSince",Je(u.measurement_started_at)):c("notMeasured")})]}),e("label",{className:"smb-value-period",children:[e("span",{children:c("period")}),e("select",{value:p,onChange:i=>m(Number(i.target.value)),children:[7,30,90].map(i=>e("option",{value:i,children:c("lastDays",i)},i))})]})]}),h&&e("p",{role:"status",className:"smb-value-status",children:c("loading")}),x&&e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:c("loadError")})}),u&&!x&&e("div",{className:"smb-value-grid","aria-busy":h,children:[e(Ze,{cost:u.cost}),e(Qe,{orders:u.orders,allowed:u.revenue_allowed}),e(Xe,{outcomes:u.outcomes,sources:u.handoffs_by_source})]}),_&&e("details",{className:"smb-value-settings",children:[e("summary",{children:c("settingsTitle")}),e("form",{onSubmit:$,children:[e("fieldset",{children:[e("legend",{children:c("costTitle")}),e("p",{className:"description",children:c("refHelp")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[c("refLabel"),e("input",{type:"text",maxLength:80,value:_.reference.label,onChange:i=>v("label",i.target.value)})]}),e("label",{children:[c("refUnit"),e("select",{value:_.reference.unit,onChange:i=>v("unit",i.target.value),children:Object.keys(ae).map(i=>e("option",{value:i,children:c(ae[i])},i))})]}),e("label",{children:[c("refPrice"),e("input",{type:"number",min:"0",step:"0.0001",value:_.reference.price,onChange:i=>v("price",Number(i.target.value))})]}),e("label",{children:[c("refSource"),e("input",{type:"text",maxLength:200,value:_.reference.source,onChange:i=>v("source",i.target.value)})]}),e("label",{children:[c("refDate"),e("input",{type:"date",value:_.reference.date,onChange:i=>v("date",i.target.value)})]}),e("label",{children:[c("pluginMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:_.plugin_cost_monthly,onChange:i=>N({..._,plugin_cost_monthly:Number(i.target.value)})})]}),e("label",{children:[c("otherMonthly"),e("input",{type:"number",min:"0",step:"0.01",value:_.other_cost_monthly,onChange:i=>N({..._,other_cost_monthly:Number(i.target.value)})})]})]})]}),e("fieldset",{children:[e("legend",{children:c("ordersTitle")}),e("div",{className:"smb-value-fields",children:[e("label",{children:[c("window"),e("select",{value:_.window_hours,onChange:i=>N({..._,window_hours:Number(i.target.value)}),"aria-describedby":"smb-window-help",children:[24,72,168].map(i=>e("option",{value:i,children:c(Ce[i])},i))}),e("span",{id:"smb-window-help",className:"description",children:c("windowHelp")})]}),e("div",{role:"group","aria-labelledby":"smb-statuses-label",children:[e("span",{id:"smb-statuses-label",className:"smb-value-group-label",children:c("statuses")}),Object.keys(ne).map(i=>e("label",{className:"smb-value-check",children:[e("input",{type:"checkbox",checked:_.statuses.includes(i),onChange:l=>N({..._,statuses:l.target.checked?[..._.statuses,i]:_.statuses.filter(R=>R!==i)})}),c(ne[i])]},i))]})]})]}),e("button",{type:"submit",className:"button button-primary",disabled:C,children:c(C?"saving":"save")})]})]})]})}function Ze({cost:r}){const t=r.reference;return e("article",{className:"smb-value-card","aria-labelledby":"smb-cost-title",children:[e("h3",{id:"smb-cost-title",children:c("costTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.ai_conversations)}),e("dl",{className:"smb-value-rows",children:[e("div",{children:[e("dt",{children:c("aiCost")}),e("dd",{children:[e("strong",{children:j(r.ai_cost_total)}),e("span",{className:"smb-value-sub",children:c("aiCostSplit",j(r.ai_cost_usage),j(r.ai_cost_estimated))}),r.unpriced_requests>0&&e("span",{className:"smb-value-sub",children:c("unpriced",r.unpriced_requests)})]})]}),e("div",{children:[e("dt",{children:c("pluginCost")}),e("dd",{children:j(r.plugin_cost)})]}),e("div",{children:[e("dt",{children:c("otherCost")}),e("dd",{children:j(r.other_cost)})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:c("totalCost")}),e("dd",{children:e("strong",{children:j(r.total_cost)})})]}),t&&e(D,{children:[e("div",{children:[e("dt",{children:c("referenceCost",t.label)}),e("dd",{children:[j(t.cost),e("span",{className:"smb-value-sub",children:[c("referenceVolume",t.volume,j(t.price))," ",c(ae[t.unit]||"unitAiConversation")]}),t.source&&e("span",{className:"smb-value-sub",children:[c("referenceSource",t.source),t.date?` · ${t.date}`:""]})]})]}),e("div",{className:"smb-value-total",children:[e("dt",{children:c("difference")}),e("dd",{children:e("strong",{children:j(t.difference)})})]})]})]}),e("p",{className:"smb-value-note",children:c(t?"differenceNote":"noReference")}),e("p",{className:"smb-value-note",children:c("costScope")})]})}function Qe({orders:r,allowed:t}){const n=r.revenue_by_currency||{},p=(r.statuses||[]).map(m=>c(ne[m]||m)).join(", ");return e("article",{className:"smb-value-card","aria-labelledby":"smb-orders-title",children:[e("h3",{id:"smb-orders-title",children:c("ordersTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.assisted_orders)}),e("p",{className:"smb-value-big",children:[e("strong",{children:r.assisted_orders})," ",e("span",{children:c("orders")})]}),e("p",{className:"smb-value-caption",children:c("assistedCaption")}),e("h4",{children:c("revenue")}),t?Object.keys(n).length>0?e("ul",{className:"smb-value-list",children:Object.entries(n).map(([m,u])=>e("li",{children:e("strong",{children:Ve(u,m)})},m))}):e("p",{children:c("noRevenue")}):e("p",{children:c("revenueLocked")}),t&&e("p",{className:"smb-value-note",children:c("revenueMethod")}),e("p",{className:"smb-value-note",children:c("orderRule",c(Ce[r.window_hours]||"hours24"),p)})]})}function Xe({outcomes:r,sources:t}){const n=[["confirmed",r.resolved_confirmed,r.counts.resolved_confirmed],["inferred",r.resolved_inferred,r.counts.resolved_inferred],["handedOff",r.handed_off,r.counts.handed_off],["otherOutcome",r.unresolved_or_unknown,r.counts.unresolved+r.counts.abandoned+r.counts.unknown]];return e("article",{className:"smb-value-card","aria-labelledby":"smb-outcomes-title",children:[e("h3",{id:"smb-outcomes-title",children:c("outcomesTitle")}),e("p",{className:"smb-value-sample",children:c("sample",r.finished)}),r.insufficient_data?e("p",{className:"smb-value-empty",children:c("notEnough",r.finished,30)}):e("dl",{className:"smb-value-rows",children:n.map(([p,m,u])=>e("div",{children:[e("dt",{children:c(p)}),e("dd",{children:[e("strong",{children:m?`${m.rate}%`:"—"})," ",e("span",{className:"smb-value-sub-inline",children:["(",u,")"]}),(m==null?void 0:m.ci95)&&e("span",{className:"smb-value-sub",children:c("ci",m.ci95.low,m.ci95.high)})]})]},p))}),e("p",{className:"smb-value-note",children:c("outcomeNote")}),e("h4",{children:c("bySource")}),e("ul",{className:"smb-value-list smb-value-sources",children:Ke.map(([p,m])=>e("li",{children:[e("span",{children:c(m)})," ",e("strong",{children:(t==null?void 0:t[p])??0})]},p))})]})}const et=1,tt="./admin-insights.bundle.js";let ee=null;function st(){if(!ee){const r=new URL(import.meta.url);ee=import(new URL(tt+r.search,r).href).then(n=>{if(!n||n.API!==et)throw new Error("Sales Insights module mismatch");return n}).catch(n=>{throw ee=null,n})}return ee}const ve=["value","insights","inbox","rules","leads"],Se="smbcommerce_admin_tab";function rt(){const r=new URLSearchParams(window.location.search).get("smb_tab");if(r&&ve.includes(r))return r;try{const t=window.localStorage.getItem(Se);return t&&ve.includes(t)?t:"value"}catch{return"value"}}const nt=()=>{var C,I,L,$;const[r,t]=d(rt),[n,p]=d(null),[m,u]=d(!1),[k,h]=d(null),g=v=>{t(v);try{window.localStorage.setItem(Se,v)}catch{}},x=((I=(C=window.SMBCommerceAdmin)==null?void 0:C.i18n)==null?void 0:I.tab)||"AI Value & Performance",T=(($=(L=window.SMBCommerceAdmin)==null?void 0:L.i18n)==null?void 0:$.insightsTab)||"Sales Insights";F(()=>{r!=="insights"||n||(u(!1),st().then(v=>p(()=>v)).catch(()=>u(!0)))},[r,n]);const _=v=>{h(v),g("inbox")},N=(v,i)=>e("button",{className:`nav-tab ${r===v?"nav-tab-active":""}`,onClick:()=>g(v),"aria-current":r===v?"page":void 0,children:i});return e("div",{className:"smb-commerce-admin-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Admin"}),e("h2",{className:"nav-tab-wrapper",children:[N("value",x),N("insights",T),N("inbox","Inbox"),N("rules","Rules Engine"),N("leads","Leads")]}),e("div",{style:{marginTop:20},children:[r==="value"&&e(Ye,{}),r==="insights"&&(n?e(n.SalesInsights,{onOpenConversation:_}):m?e("div",{className:"notice notice-error smb-inline-notice",role:"alert",children:e("p",{children:be("loadError")})}):e("p",{role:"status",className:"smb-value-status",children:be("loading")})),r==="inbox"&&e(De,{openId:k}),r==="rules"&&e(je,{}),r==="leads"&&e(Ue,{})]})]})},at=()=>e("div",{className:"smb-commerce-settings-wrapper",children:[e("h1",{children:"Ninhle AI Sales Assistant Settings"}),e(qe,{})]}),fe=document.getElementById("smb-commerce-admin-root");fe&&Ne(fe).render(e(nt,{}));const ke=document.getElementById("smb-commerce-settings-root");ke&&Ne(ke).render(e(at,{}));
  • ninhle-ai-sales-assistant/trunk/assets/dist/admin.css

    r3752682 r3765552  
    1 .smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{margin:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.smb-commerce-settings-layout{display:grid;grid-template-columns:1fr 380px;gap:20px;align-items:start}@media (max-width: 1024px){.smb-commerce-settings-layout{grid-template-columns:1fr}}.smb-commerce-settings-form-col{width:100%}.smb-commerce-settings-preview-col{position:relative}.preview-sticky{position:fixed;bottom:20px;right:20px;z-index:99999;width:350px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 10px 25px #00000026}.preview-sticky h3{margin-top:0;color:#1e293b;font-size:1.1rem}.widget-preview-box{background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 10px 20px #0000001a;display:flex;flex-direction:column;height:400px;border:1px solid #e2e8f0;margin-top:12px}.widget-preview-header{display:flex;justify-content:space-between;align-items:center;padding:16px;color:#fff}.widget-preview-header-brand{display:flex;align-items:center;gap:12px;font-weight:600;font-size:15px}.widget-preview-logo{width:32px;height:32px;border-radius:50%;object-fit:cover;background:#fff3}.widget-preview-close{font-size:24px;line-height:1;opacity:.8}.widget-preview-body{flex:1;background:#f8fafc;padding:16px;display:flex;flex-direction:column;gap:12px}.preview-message{padding:12px 16px;border-radius:16px;font-size:14px;line-height:1.4;max-width:85%}.preview-message.bot{background:#fff;align-self:flex-start;border-bottom-left-radius:4px;box-shadow:0 1px 2px #0000000d;color:#334155}.preview-message.user{align-self:flex-end;border-bottom-right-radius:4px;box-shadow:0 1px 2px #0000001a}.widget-preview-footer{padding:12px;background:#fff;border-top:1px solid #e2e8f0;display:flex;gap:8px}.widget-preview-footer input{flex:1;border:1px solid #e2e8f0;border-radius:20px;padding:8px 16px;background:#f8fafc;font-size:14px;outline:none}.widget-preview-footer button{width:36px;height:36px;border-radius:50%;border:none;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}.smb-commerce-card{background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 3px #0000001a,0 1px 2px #0000000f;padding:20px;margin-bottom:20px;border-radius:8px;transition:box-shadow .2s}.smb-commerce-card:hover{box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.smb-commerce-card h3{margin-top:0;padding-bottom:12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;color:#0f172a;font-size:1.1rem}.smb-commerce-form-group{margin-bottom:16px}.smb-commerce-form-group-row{display:flex;gap:20px}.smb-commerce-form-group-row>div{flex:1}.smb-commerce-form-group label{display:block;font-weight:600;margin-bottom:8px;color:#334155}.smb-commerce-form-group input[type=text],.smb-commerce-form-group input[type=password],.smb-commerce-form-group select{width:100%;max-width:none;padding:10px 12px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;box-shadow:0 1px 2px #0000000d inset;transition:border-color .2s,box-shadow .2s}.smb-commerce-form-group input[type=text]:focus,.smb-commerce-form-group input[type=password]:focus,.smb-commerce-form-group select:focus{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a;outline:none}.color-picker-wrapper{display:flex;align-items:center;gap:12px}.color-picker-wrapper input[type=color]{width:48px;height:48px;padding:0;border:2px solid #e2e8f0;border-radius:8px;cursor:pointer;background:none}.color-picker-wrapper .color-hex{font-family:monospace;background:#f1f5f9;padding:6px 12px;border-radius:4px;border:1px solid #e2e8f0}.toggle-group{display:flex;gap:16px;align-items:flex-start}.toggle-switch{position:relative;display:inline-block;width:50px;height:28px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5e1;transition:.4s;border-radius:34px}.toggle-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%;box-shadow:0 2px 4px #0003}input:checked+.toggle-slider{background-color:#01696f}input:checked+.toggle-slider:before{transform:translate(22px)}.smb-commerce-notice{background:#fff;border-left:4px solid #00a32a;box-shadow:0 1px 1px #0000000a;padding:1px 12px;margin-bottom:20px}.smb-commerce-inbox-container{display:flex;height:70vh;background:#fff;border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px #0000000a;overflow:hidden}.smb-commerce-sidebar{width:300px;border-right:1px solid #ccd0d4;overflow-y:auto;background:#f9f9f9}.smb-commerce-conversation-item{padding:15px;border-bottom:1px solid #e2e4e7;cursor:pointer;transition:background .2s}.smb-commerce-conversation-item:hover,.smb-commerce-conversation-item.active{background:#fff;border-left:3px solid #2271b1}.smb-commerce-main{flex:1;display:flex;flex-direction:column}.smb-commerce-main-header{padding:15px;border-bottom:1px solid #ccd0d4;display:flex;justify-content:space-between;align-items:center;background:#fff}.smb-commerce-chat-history{flex:1;padding:20px;overflow-y:auto;background:#f0f0f1;display:flex;flex-direction:column;gap:10px}.smb-commerce-admin-msg{max-width:70%;padding:10px 14px;border-radius:12px}.smb-commerce-admin-msg.user{align-self:flex-start;background:#fff;border:1px solid #ddd}.smb-commerce-admin-msg.agent,.smb-commerce-admin-msg.bot{align-self:flex-end;background:#d4edda;border:1px solid #c3e6cb}.smb-commerce-main-input{padding:15px;background:#fff;border-top:1px solid #ccd0d4;display:flex;gap:10px}.smb-commerce-main-input input{flex:1;padding:8px 12px}.smb-commerce-admin-wrapper,.smb-commerce-admin-wrapper *,.smb-commerce-admin-wrapper *:before,.smb-commerce-admin-wrapper *:after,.smb-commerce-settings-wrapper,.smb-commerce-settings-wrapper *,.smb-commerce-settings-wrapper *:before,.smb-commerce-settings-wrapper *:after{box-sizing:border-box!important}.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{min-width:0}.smb-commerce-admin-wrapper td,.smb-commerce-admin-wrapper th,.smb-commerce-settings-wrapper td,.smb-commerce-settings-wrapper th,.smb-commerce-admin-msg{overflow-wrap:anywhere!important;word-break:break-word!important}.smb-notices .smb-inline-notice,.smb-inline-notice{position:relative;margin:0 0 12px}.smb-confirm{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:0 0 16px;padding:12px 16px;background:#fcf0f1;border-left:4px solid #d63638;box-shadow:0 1px 1px #0000000a}.smb-confirm p{margin:0;font-weight:600;color:#1d2327}.smb-confirm-actions{display:flex;gap:8px}.smb-commerce-admin-wrapper .button.smb-button-danger,.smb-commerce-settings-wrapper .button.smb-button-danger{background:#b32d2e;border-color:#b32d2e;color:#fff}.smb-field-error{margin:4px 0 0;color:#b32d2e}.smb-value-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:16px}.smb-value-header h2{margin:0 0 4px;font-size:1.4rem}.smb-value-intro{margin:0;max-width:70ch;color:#50575e}.smb-value-meta,.smb-value-status{margin:6px 0 0;color:#50575e}.smb-value-period{display:flex;flex-direction:column;gap:4px;font-weight:600}.smb-value-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}.smb-value-card{background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:20px}.smb-value-card h3{margin:0;font-size:1.05rem;color:#1d2327}.smb-value-card h4{margin:18px 0 6px;font-size:.95rem}.smb-value-sample{margin:2px 0 12px;font-size:12px;color:#646970}.smb-value-rows{margin:0}.smb-value-rows>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:8px 0;border-top:1px solid #f0f0f1}.smb-value-rows dt{color:#3c434a}.smb-value-rows dd{margin:0;text-align:right}.smb-value-total dt,.smb-value-total dd{font-weight:600}.smb-value-sub{display:block;margin-top:2px;font-size:12px;color:#646970}.smb-value-sub-inline{font-size:12px;color:#646970}.smb-value-big{margin:4px 0;font-size:14px}.smb-value-big strong{font-size:2rem;line-height:1.1;margin-right:4px}.smb-value-caption{margin:4px 0 0;font-weight:600;color:#3c434a}.smb-value-note,.smb-value-empty{margin:10px 0 0;font-size:12px;color:#50575e}.smb-value-empty{font-size:13px}.smb-value-list{margin:0;padding:0;list-style:none}.smb-value-sources li{display:flex;justify-content:space-between;padding:4px 0;border-top:1px solid #f0f0f1}.smb-value-settings{margin-top:20px;background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:0 20px}.smb-value-settings summary{padding:14px 0;font-weight:600;cursor:pointer}.smb-value-settings form{padding-bottom:20px}.smb-value-settings fieldset{margin:0 0 16px;padding:0;border:0}.smb-value-settings legend{margin-bottom:6px;font-weight:600}.smb-value-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px 16px}.smb-value-fields>label{display:flex;flex-direction:column;gap:4px}.smb-value-group-label{display:block;margin-bottom:4px}.smb-value-check{display:flex;align-items:center;gap:6px;min-height:24px}.smb-value-settings summary:focus-visible,.smb-value-period select:focus-visible{outline:2px solid #2271b1;outline-offset:2px}
     1.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{margin:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.smb-commerce-settings-layout{display:grid;grid-template-columns:1fr 380px;gap:20px;align-items:start}@media (max-width: 1024px){.smb-commerce-settings-layout{grid-template-columns:1fr}}.smb-commerce-settings-form-col{width:100%}.smb-commerce-settings-preview-col{position:relative}.preview-sticky{position:fixed;bottom:20px;right:20px;z-index:99999;width:350px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 10px 25px #00000026}.preview-sticky h3{margin-top:0;color:#1e293b;font-size:1.1rem}.widget-preview-box{background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 10px 20px #0000001a;display:flex;flex-direction:column;height:400px;border:1px solid #e2e8f0;margin-top:12px}.widget-preview-header{display:flex;justify-content:space-between;align-items:center;padding:16px;color:#fff}.widget-preview-header-brand{display:flex;align-items:center;gap:12px;font-weight:600;font-size:15px}.widget-preview-logo{width:32px;height:32px;border-radius:50%;object-fit:cover;background:#fff3}.widget-preview-close{font-size:24px;line-height:1;opacity:.8}.widget-preview-body{flex:1;background:#f8fafc;padding:16px;display:flex;flex-direction:column;gap:12px}.preview-message{padding:12px 16px;border-radius:16px;font-size:14px;line-height:1.4;max-width:85%}.preview-message.bot{background:#fff;align-self:flex-start;border-bottom-left-radius:4px;box-shadow:0 1px 2px #0000000d;color:#334155}.preview-message.user{align-self:flex-end;border-bottom-right-radius:4px;box-shadow:0 1px 2px #0000001a}.widget-preview-footer{padding:12px;background:#fff;border-top:1px solid #e2e8f0;display:flex;gap:8px}.widget-preview-footer input{flex:1;border:1px solid #e2e8f0;border-radius:20px;padding:8px 16px;background:#f8fafc;font-size:14px;outline:none}.widget-preview-footer button{width:36px;height:36px;border-radius:50%;border:none;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}.smb-commerce-card{background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 3px #0000001a,0 1px 2px #0000000f;padding:20px;margin-bottom:20px;border-radius:8px;transition:box-shadow .2s}.smb-commerce-card:hover{box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.smb-commerce-card h3{margin-top:0;padding-bottom:12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;color:#0f172a;font-size:1.1rem}.smb-commerce-form-group{margin-bottom:16px}.smb-commerce-form-group-row{display:flex;gap:20px}.smb-commerce-form-group-row>div{flex:1}.smb-commerce-form-group label{display:block;font-weight:600;margin-bottom:8px;color:#334155}.smb-commerce-form-group input[type=text],.smb-commerce-form-group input[type=password],.smb-commerce-form-group select{width:100%;max-width:none;padding:10px 12px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;box-shadow:0 1px 2px #0000000d inset;transition:border-color .2s,box-shadow .2s}.smb-commerce-form-group input[type=text]:focus,.smb-commerce-form-group input[type=password]:focus,.smb-commerce-form-group select:focus{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a;outline:none}.color-picker-wrapper{display:flex;align-items:center;gap:12px}.color-picker-wrapper input[type=color]{width:48px;height:48px;padding:0;border:2px solid #e2e8f0;border-radius:8px;cursor:pointer;background:none}.color-picker-wrapper .color-hex{font-family:monospace;background:#f1f5f9;padding:6px 12px;border-radius:4px;border:1px solid #e2e8f0}.toggle-group{display:flex;gap:16px;align-items:flex-start}.toggle-switch{position:relative;display:inline-block;width:50px;height:28px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5e1;transition:.4s;border-radius:34px}.toggle-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%;box-shadow:0 2px 4px #0003}input:checked+.toggle-slider{background-color:#01696f}input:checked+.toggle-slider:before{transform:translate(22px)}.smb-commerce-notice{background:#fff;border-left:4px solid #00a32a;box-shadow:0 1px 1px #0000000a;padding:1px 12px;margin-bottom:20px}.smb-commerce-inbox-container{display:flex;height:70vh;background:#fff;border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px #0000000a;overflow:hidden}.smb-commerce-sidebar{width:300px;border-right:1px solid #ccd0d4;overflow-y:auto;background:#f9f9f9}.smb-commerce-conversation-item{padding:15px;border-bottom:1px solid #e2e4e7;cursor:pointer;transition:background .2s}.smb-commerce-conversation-item:hover,.smb-commerce-conversation-item.active{background:#fff;border-left:3px solid #2271b1}.smb-commerce-main{flex:1;display:flex;flex-direction:column}.smb-commerce-main-header{padding:15px;border-bottom:1px solid #ccd0d4;display:flex;justify-content:space-between;align-items:center;background:#fff}.smb-commerce-chat-history{flex:1;padding:20px;overflow-y:auto;background:#f0f0f1;display:flex;flex-direction:column;gap:10px}.smb-commerce-admin-msg{max-width:70%;padding:10px 14px;border-radius:12px}.smb-commerce-admin-msg.user{align-self:flex-start;background:#fff;border:1px solid #ddd}.smb-commerce-admin-msg.agent,.smb-commerce-admin-msg.bot{align-self:flex-end;background:#d4edda;border:1px solid #c3e6cb}.smb-customer-aside{width:260px;flex-shrink:0;border-left:1px solid #ccd0d4;overflow-y:auto;background:#fff;padding:14px}.smb-customer-inline{display:none;border-bottom:1px solid #ccd0d4;padding:8px 15px;background:#f6f7f7}.smb-customer-inline summary{cursor:pointer;font-weight:600}.smb-customer-card h4{margin:0 0 8px;font-size:14px}.smb-customer-card dl{margin:0}.smb-customer-card dt{margin-top:10px;color:#646970;font-size:12px}.smb-customer-card dd{margin:2px 0 0;overflow-wrap:anywhere}.smb-customer-card ul{margin:0;padding-left:16px;list-style:disc}.smb-customer-sub,.smb-customer-note{color:#646970;font-size:12px}.smb-customer-links{margin:12px 0 0}.smb-customer-warn{margin:0 0 8px;padding:6px 8px;background:#fcf9e8;border-left:3px solid #dba617;font-size:12px}@media (max-width: 1100px){.smb-customer-aside{display:none}.smb-customer-inline{display:block}}.smb-commerce-main-input{padding:15px;background:#fff;border-top:1px solid #ccd0d4;display:flex;gap:10px}.smb-commerce-main-input input{flex:1;padding:8px 12px}.smb-commerce-admin-wrapper,.smb-commerce-admin-wrapper *,.smb-commerce-admin-wrapper *:before,.smb-commerce-admin-wrapper *:after,.smb-commerce-settings-wrapper,.smb-commerce-settings-wrapper *,.smb-commerce-settings-wrapper *:before,.smb-commerce-settings-wrapper *:after{box-sizing:border-box!important}.smb-commerce-admin-wrapper,.smb-commerce-settings-wrapper{min-width:0}.smb-commerce-admin-wrapper td,.smb-commerce-admin-wrapper th,.smb-commerce-settings-wrapper td,.smb-commerce-settings-wrapper th,.smb-commerce-admin-msg{overflow-wrap:anywhere!important;word-break:break-word!important}.smb-notices .smb-inline-notice,.smb-inline-notice{position:relative;margin:0 0 12px}.smb-confirm{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:0 0 16px;padding:12px 16px;background:#fcf0f1;border-left:4px solid #d63638;box-shadow:0 1px 1px #0000000a}.smb-confirm p{margin:0;font-weight:600;color:#1d2327}.smb-confirm-actions{display:flex;gap:8px}.smb-commerce-admin-wrapper .button.smb-button-danger,.smb-commerce-settings-wrapper .button.smb-button-danger{background:#b32d2e;border-color:#b32d2e;color:#fff}.smb-field-error{margin:4px 0 0;color:#b32d2e}.smb-value-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:16px}.smb-value-header h2{margin:0 0 4px;font-size:1.4rem}.smb-value-intro{margin:0;max-width:70ch;color:#50575e}.smb-value-meta,.smb-value-status{margin:6px 0 0;color:#50575e}.smb-value-period{display:flex;flex-direction:column;gap:4px;font-weight:600}.smb-value-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}.smb-value-card{background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:20px}.smb-value-card h3{margin:0;font-size:1.05rem;color:#1d2327}.smb-value-card h4{margin:18px 0 6px;font-size:.95rem}.smb-value-sample{margin:2px 0 12px;font-size:12px;color:#646970}.smb-value-rows{margin:0}.smb-value-rows>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:8px 0;border-top:1px solid #f0f0f1}.smb-value-rows dt{color:#3c434a}.smb-value-rows dd{margin:0;text-align:right}.smb-value-total dt,.smb-value-total dd{font-weight:600}.smb-value-sub{display:block;margin-top:2px;font-size:12px;color:#646970}.smb-value-sub-inline{font-size:12px;color:#646970}.smb-value-big{margin:4px 0;font-size:14px}.smb-value-big strong{font-size:2rem;line-height:1.1;margin-right:4px}.smb-value-caption{margin:4px 0 0;font-weight:600;color:#3c434a}.smb-value-note,.smb-value-empty{margin:10px 0 0;font-size:12px;color:#50575e}.smb-value-empty{font-size:13px}.smb-value-list{margin:0;padding:0;list-style:none}.smb-value-sources li{display:flex;justify-content:space-between;padding:4px 0;border-top:1px solid #f0f0f1}.smb-value-settings{margin-top:20px;background:#fff;border-radius:8px;box-shadow:0 1px 3px #0f172a1f;padding:0 20px}.smb-value-settings summary{padding:14px 0;font-weight:600;cursor:pointer}.smb-value-settings form{padding-bottom:20px}.smb-value-settings fieldset{margin:0 0 16px;padding:0;border:0}.smb-value-settings legend{margin-bottom:6px;font-weight:600}.smb-value-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px 16px}.smb-value-fields>label{display:flex;flex-direction:column;gap:4px}.smb-value-group-label{display:block;margin-bottom:4px}.smb-value-check{display:flex;align-items:center;gap:6px;min-height:24px}.smb-value-settings summary:focus-visible,.smb-value-period select:focus-visible{outline:2px solid #2271b1;outline-offset:2px}.smb-insights-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;align-items:start}.smb-insights-wide{grid-column:1 / -1}.smb-insights-table td{vertical-align:top}.smb-insights-table td:nth-child(2),.smb-insights-table td:nth-child(3){white-space:nowrap}.smb-insights-table td strong{overflow-wrap:anywhere}.smb-insights-actions{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:flex-end}.smb-insights-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.smb-insights-tag{padding:1px 8px;border-radius:999px;background:#f0f0f1;color:#50575e;font-size:11px}.smb-insights-draft form{display:grid;gap:8px;max-width:720px}.smb-insights-draft label{display:grid;gap:4px;font-weight:600}.smb-insights-draft fieldset{margin:0;padding:0;border:0}.smb-insights-draft fieldset label{display:flex;font-weight:400}.smb-insights-notes ul{margin:0}.smb-insights-notes li{padding:6px 0;border-top:1px solid #f0f0f1}.smb-insights-list li{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-top:1px solid #f0f0f1}.smb-insights-list li>span:first-child{min-width:0;overflow-wrap:anywhere}.smb-insights-list li>span:last-child{flex-shrink:0;text-align:right}.smb-insights-list a,.smb-insights-list .button-link,.smb-insights-actions .button-link{white-space:nowrap}.smb-insights-why{margin:0}.smb-insights-why li{padding:8px 0;border-top:1px solid #f0f0f1}.smb-insights-why-row{display:flex;justify-content:space-between;gap:12px}.smb-insights-bar{height:6px;margin-top:4px;border-radius:999px;background:#f0f0f1;overflow:hidden}.smb-insights-bar span{display:block;height:100%;background:#2271b1}.smb-insights-quote{margin:4px 0 0;padding-left:8px;border-left:3px solid #dcdcde;color:#50575e;font-size:12px;overflow-wrap:anywhere}@media (max-width: 782px){.smb-insights-table thead{display:none}.smb-insights-table tr,.smb-insights-table td{display:block;width:auto}.smb-insights-table td[data-label]:before{content:attr(data-label) ": ";color:#646970}.smb-insights-table td.smb-insights-actions{display:flex;justify-content:flex-start}.smb-insights-list li>span:last-child{flex-shrink:1}}
  • ninhle-ai-sales-assistant/trunk/assets/dist/manifest.json

    r3761175 r3765552  
    11{
    2   "_hooks.module-C33f2O-M.js": {
    3     "file": "assets/hooks.module-C33f2O-M.js",
    4     "name": "hooks.module"
    5   },
    6   "_widget-shared-BrY9zvIQ.js": {
    7     "file": "assets/widget-shared-BrY9zvIQ.js",
    8     "name": "widget-shared"
    9   },
    10   "src/JS/admin/index.tsx": {
    11     "file": "admin.bundle.js",
    12     "name": "admin",
    13     "src": "src/JS/admin/index.tsx",
    14     "isEntry": true,
    15     "imports": [
    16       "_hooks.module-C33f2O-M.js"
    17     ],
    18     "css": [
    19       "admin.css"
    20     ]
    21   },
    22   "src/JS/widget/components/ChatWindow.tsx": {
    23     "file": "assets/ChatWindow-BtSh_sym.js",
    24     "name": "ChatWindow",
    25     "src": "src/JS/widget/components/ChatWindow.tsx",
    26     "isDynamicEntry": true,
    27     "imports": [
    28       "_widget-shared-BrY9zvIQ.js",
    29       "_hooks.module-C33f2O-M.js"
    30     ],
    31     "dynamicImports": [
    32       "src/JS/widget/voice.ts"
    33     ]
    34   },
    35   "src/JS/widget/index.tsx": {
    36     "file": "widget.bundle.js",
    37     "name": "widget",
    38     "src": "src/JS/widget/index.tsx",
    39     "isEntry": true,
    40     "imports": [
    41       "_widget-shared-BrY9zvIQ.js",
    42       "_hooks.module-C33f2O-M.js"
    43     ],
    44     "dynamicImports": [
    45       "src/JS/widget/components/ChatWindow.tsx"
    46     ],
    47     "css": [
    48       "widget.css"
    49     ]
    50   },
    51   "src/JS/widget/voice.ts": {
    52     "file": "assets/voice-DtEIuG21.js",
    53     "name": "voice",
    54     "src": "src/JS/widget/voice.ts",
    55     "isDynamicEntry": true
    56   }
     2    "_ChatWindow-D3sL_SAs.js":  {
     3                                    "file":  "assets/ChatWindow-D3sL_SAs.js",
     4                                    "name":  "ChatWindow",
     5                                    "isDynamicEntry":  true,
     6                                    "imports":  [
     7                                                    "_widget-shared-CHYtJpo8.js",
     8                                                    "_hooks.module-C33f2O-M.js"
     9                                                ],
     10                                    "dynamicImports":  [
     11                                                           "src/JS/widget/voice.ts",
     12                                                           "src/JS/widget/components/ProductCards.tsx",
     13                                                           "src/JS/widget/components/CartPanel.tsx"
     14                                                       ]
     15                                },
     16    "_hooks.module-C33f2O-M.js":  {
     17                                      "file":  "assets/hooks.module-C33f2O-M.js",
     18                                      "name":  "hooks.module"
     19                                  },
     20    "_i18n-D_zElFbw.js":  {
     21                              "file":  "assets/i18n-D_zElFbw.js",
     22                              "name":  "i18n",
     23                              "imports":  [
     24                                              "_hooks.module-C33f2O-M.js"
     25                                          ]
     26                          },
     27    "_pro-CgluFZP2.js":  {
     28                             "file":  "assets/pro-CgluFZP2.js",
     29                             "name":  "pro",
     30                             "imports":  [
     31                                             "_hooks.module-C33f2O-M.js"
     32                                         ]
     33                         },
     34    "_widget-shared-CHYtJpo8.js":  {
     35                                       "file":  "assets/widget-shared-CHYtJpo8.js",
     36                                       "name":  "widget-shared"
     37                                   },
     38    "src/JS/admin/index.tsx":  {
     39                                   "file":  "admin.bundle.js",
     40                                   "name":  "admin",
     41                                   "src":  "src/JS/admin/index.tsx",
     42                                   "isEntry":  true,
     43                                   "imports":  [
     44                                                   "_hooks.module-C33f2O-M.js",
     45                                                   "_i18n-D_zElFbw.js"
     46                                               ],
     47                                   "css":  [
     48                                               "admin.css"
     49                                           ]
     50                               },
     51    "src/JS/admin/insights/index.tsx":  {
     52                                            "file":  "admin-insights.bundle.js",
     53                                            "name":  "admin-insights",
     54                                            "src":  "src/JS/admin/insights/index.tsx",
     55                                            "isEntry":  true,
     56                                            "imports":  [
     57                                                            "_hooks.module-C33f2O-M.js",
     58                                                            "_i18n-D_zElFbw.js"
     59                                                        ]
     60                                        },
     61    "src/JS/widget/cart.ts":  {
     62                                  "file":  "assets/cart-BtM4a74e.js",
     63                                  "name":  "cart",
     64                                  "src":  "src/JS/widget/cart.ts",
     65                                  "isDynamicEntry":  true
     66                              },
     67    "src/JS/widget/components/CartPanel.tsx":  {
     68                                                   "file":  "assets/CartPanel-D6-1EaTY.js",
     69                                                   "name":  "CartPanel",
     70                                                   "src":  "src/JS/widget/components/CartPanel.tsx",
     71                                                   "isDynamicEntry":  true,
     72                                                   "imports":  [
     73                                                                   "_hooks.module-C33f2O-M.js",
     74                                                                   "_widget-shared-CHYtJpo8.js",
     75                                                                   "src/JS/widget/cart.ts",
     76                                                                   "_pro-CgluFZP2.js"
     77                                                               ]
     78                                               },
     79    "src/JS/widget/components/ProductCards.tsx":  {
     80                                                      "file":  "assets/ProductCards-81V8biLP.js",
     81                                                      "name":  "ProductCards",
     82                                                      "src":  "src/JS/widget/components/ProductCards.tsx",
     83                                                      "isDynamicEntry":  true,
     84                                                      "imports":  [
     85                                                                      "_widget-shared-CHYtJpo8.js",
     86                                                                      "_hooks.module-C33f2O-M.js",
     87                                                                      "_ChatWindow-D3sL_SAs.js",
     88                                                                      "_pro-CgluFZP2.js"
     89                                                                  ],
     90                                                      "dynamicImports":  [
     91                                                                             "src/JS/widget/cart.ts"
     92                                                                         ]
     93                                                  },
     94    "src/JS/widget/index.tsx":  {
     95                                    "file":  "widget.bundle.js",
     96                                    "name":  "widget",
     97                                    "src":  "src/JS/widget/index.tsx",
     98                                    "isEntry":  true,
     99                                    "imports":  [
     100                                                    "_widget-shared-CHYtJpo8.js",
     101                                                    "_hooks.module-C33f2O-M.js"
     102                                                ],
     103                                    "dynamicImports":  [
     104                                                           "_ChatWindow-D3sL_SAs.js"
     105                                                       ],
     106                                    "css":  [
     107                                                "widget.css"
     108                                            ]
     109                                },
     110    "src/JS/widget/voice.ts":  {
     111                                   "file":  "assets/voice-DkonJhm8.js",
     112                                   "name":  "voice",
     113                                   "src":  "src/JS/widget/voice.ts",
     114                                   "isDynamicEntry":  true
     115                               }
    57116}
  • ninhle-ai-sales-assistant/trunk/assets/dist/widget.bundle.js

    r3761175 r3765552  
    1 const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./assets/ChatWindow-BtSh_sym.js","./assets/widget-shared-BrY9zvIQ.js","./assets/hooks.module-C33f2O-M.js"])))=>i.map(i=>d[i]);
    2 import{t as G,_ as J,a as D}from"./assets/widget-shared-BrY9zvIQ.js";import{u,R as Q,d as l,A as C,h as y}from"./assets/hooks.module-C33f2O-M.js";function X({onClick:s,onIntent:a,config:o,buttonRef:n}){return u("button",{type:"button",ref:n,className:`smb-launcher ${o!=null&&o.logoUrl?"has-logo":""}`,onClick:s,onPointerEnter:a,onTouchStart:a,onFocus:a,"aria-label":G("openChat"),"aria-haspopup":"dialog",children:o!=null&&o.logoUrl?u("img",{src:o.logoUrl,alt:""}):u("svg",{width:"26",height:"26",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:u("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})})})}function Y(s){const a=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s.trim());if(!a)return null;let o=a[1];o.length===3&&(o=o.split("").map(d=>d+d).join(""));const[n,_,h]=[0,2,4].map(d=>{const p=parseInt(o.slice(d,d+2),16)/255;return p<=.03928?p/12.92:Math.pow((p+.055)/1.055,2.4)});return .2126*n+.7152*_+.0722*h}const Z="#111111",ee=.0056;function te(s){const a=Y(s);if(a===null)return"#ffffff";const o=1.05/(a+.05),n=(a+.05)/(ee+.05);return o>=n?"#ffffff":Z}let E=null;function H(){return E||(E=J(()=>import("./assets/ChatWindow-BtSh_sym.js"),__vite__mapDeps([0,1,2]),import.meta.url).catch(s=>{throw E=null,s})),E}const ne=()=>{var T;const[s,a]=l(!1),o=C(!1),[n,_]=l(((T=window.SMBCommerceWidget)==null?void 0:T.widgetConfig)||null),[h,d]=l(null),[p,A]=l([]),[j,I]=l("bot_active"),[q,M]=l(null),k=C(null),L=C(null),N=C(!1),[$,W]=l(null),z=()=>{H().then(W).catch(()=>{})},F=document.body.classList,R=F.contains("woocommerce-checkout")||F.contains("woocommerce-cart");y(()=>{const e=window.visualViewport,t=k.current;if(!e||!t)return;let i=0;const m=()=>{if(i=0,!(window.innerWidth>document.documentElement.clientWidth+1)||t.classList.contains("is-open")){t.style.left=t.style.top=t.style.right=t.style.bottom="";return}const f=12,b=t.classList.contains("is-commerce-page")&&e.width<768?96:f;t.style.right="auto",t.style.bottom="auto",t.style.left=Math.max(e.offsetLeft+f,e.offsetLeft+e.width-t.offsetWidth-f)+"px",t.style.top=Math.max(e.offsetTop+f,e.offsetTop+e.height-t.offsetHeight-b)+"px"},c=()=>{i||(i=requestAnimationFrame(m))};e.addEventListener("resize",c),e.addEventListener("scroll",c),window.addEventListener("resize",c);const r=typeof ResizeObserver<"u"?new ResizeObserver(c):null;return r==null||r.observe(t),r==null||r.observe(document.documentElement),c(),()=>{e.removeEventListener("resize",c),e.removeEventListener("scroll",c),window.removeEventListener("resize",c),r==null||r.disconnect(),i&&cancelAnimationFrame(i)}},[]);const S=async()=>{if(h!==null)return!0;if(o.current)return!1;o.current=!0;try{const e=await D.bootstrap();return _(t=>({...t||{},...e.widget_config||{}})),d(e.conversation_id),A(e.messages||[]),I(e.status||"bot_active"),M(e.agent_name||null),!0}catch(e){return console.error("Failed to initialize Ninhle AI Sales Assistant widget",e),!1}finally{o.current=!1}};y(()=>{D.hasSession()&&(S(),z())},[]);const[x,B]=l(!1);y(()=>{if(!R||s){B(!1);return}if(typeof IntersectionObserver>"u")return;const e="#place_order, .wc-block-components-checkout-place-order-button, .checkout-button, .wc-block-cart__submit-button";let t=null;const i=new Map,m=()=>{var b;t==null||t.disconnect(),i.clear();const r=(b=L.current)==null?void 0:b.getBoundingClientRect();if(!r)return;const g=v=>`${-Math.max(0,Math.round(v))}px`,f=document.documentElement.clientWidth;t=new IntersectionObserver(v=>{v.forEach(U=>i.set(U.target,U.isIntersecting)),B(Array.from(i.values()).some(Boolean))},{rootMargin:`${g(r.top-8)} ${g(f-r.right)} ${g(window.innerHeight-r.bottom-8)} ${g(r.left)}`}),document.querySelectorAll(e).forEach(v=>t.observe(v))};m(),window.addEventListener("resize",m);const c=window.setInterval(m,2e3);return()=>{t==null||t.disconnect(),window.removeEventListener("resize",m),clearInterval(c)}},[s]),y(()=>{var e;!s&&N.current&&((e=L.current)==null||e.focus()),N.current=s},[s]);const K=async()=>{let e;try{e=await H()}catch(t){console.error("Failed to load the Ninhle AI Sales Assistant chat window",t);return}W(e),await S()&&a(!0)},w=n!=null&&n.fontFamily&&n.fontFamily!=="system"?n.fontFamily:"";y(()=>{if(w){const e=w,t=`smb-commerce-font-${e.replace(/\s+/g,"-")}`;if(!document.getElementById(t)){const i=document.createElement("link");i.id=t,i.rel="stylesheet",i.href=`https://fonts.googleapis.com/css2?family=${e.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(i)}}},[w]);const O=(n==null?void 0:n.primaryColor)||(n==null?void 0:n.primary_color)||"#01696f",P={"--smb-primary":O,"--smb-on-primary":te(O)};return w&&(P.fontFamily=`'${w}', sans-serif`),u("div",{ref:k,className:`smb-commerce-widget-container ${s?"is-open":""} ${R?"is-commerce-page":""} ${x?"is-dodging":""}`,style:P,"aria-hidden":x?"true":void 0,children:s&&h!==null&&$?u($.ChatWindow,{onClose:()=>a(!1),config:n,conversationId:h,initialMessages:p,initialStatus:j,initialAgentName:q,onSnapshot:e=>{A(e.messages),I(e.status),M(e.agentName)}}):u(X,{onClick:K,onIntent:z,config:n,buttonRef:L})})},V=document.getElementById("smb-commerce-widget-root");V&&Q(u(ne,{}),V);
     1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./assets/ChatWindow-D3sL_SAs.js","./assets/widget-shared-CHYtJpo8.js","./assets/hooks.module-C33f2O-M.js"])))=>i.map(i=>d[i]);
     2import{t as O,_ as re,a as R}from"./assets/widget-shared-CHYtJpo8.js";import{u as w,R as se,d as h,A as b,h as g}from"./assets/hooks.module-C33f2O-M.js";function oe({onClick:e,onIntent:r,config:s,buttonRef:o,unread:d=0}){return w("button",{type:"button",ref:o,className:`smb-launcher ${s!=null&&s.logoUrl?"has-logo":""}`,onClick:e,onPointerEnter:r,onTouchStart:r,onFocus:r,"aria-label":d>0?`${O("openChat")} (${O("unreadMessages").replace("%d",String(d))})`:O("openChat"),"aria-haspopup":"dialog",children:[s!=null&&s.logoUrl?w("img",{src:s.logoUrl,alt:""}):w("svg",{width:"26",height:"26",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:w("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})}),d>0&&w("span",{className:"smb-unread","aria-hidden":"true",children:d>9?"9+":d})]})}function ie(e){const r=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e.trim());if(!r)return null;let s=r[1];s.length===3&&(s=s.split("").map(p=>p+p).join(""));const[o,d,c]=[0,2,4].map(p=>{const m=parseInt(s.slice(p,p+2),16)/255;return m<=.03928?m/12.92:Math.pow((m+.055)/1.055,2.4)});return .2126*o+.7152*d+.0722*c}const ae="#111111",ce=.0056;function le(e){const r=ie(e);if(r===null)return"#ffffff";const s=1.05/(r+.05),o=(r+.05)/(ce+.05);return s>=o?"#ffffff":ae}const ue=25e3,de=30*6e4;function me(e){return e.open||!e.conversationId||!e.visible?!1:e.status==="agent_active"||e.status==="human_active"?!0:e.lastActivityAt>0&&e.now-e.lastActivityAt<de}function G(e,r){return e.filter(s=>s.sender_type!=="user"&&Number(s.id)>r).length}function C(e){return e.reduce((r,s)=>Math.max(r,Number(s.id)||0),0)}const Z="smbcommerce_seen";function fe(e){try{const r=JSON.parse(localStorage.getItem(Z)||"null");if(r&&Number(r.c)===Number(e))return{id:Number(r.id)||0,at:Number(r.at)||0}}catch{}return{id:0,at:0}}function x(e,r,s){try{localStorage.setItem(Z,JSON.stringify({c:Number(e),id:r,at:s}))}catch{}}function he(e,r){if(!r.length)return e;const s=new Set(r.map(o=>o.id));return[...e.filter(o=>!s.has(o.id)),...r].sort((o,d)=>Number(o.id)-Number(d.id))}let A=null;function Q(){return A||(A=re(()=>import("./assets/ChatWindow-D3sL_SAs.js").then(e=>e.C),__vite__mapDeps([0,1,2]),import.meta.url).catch(e=>{throw A=null,e})),A}const we=()=>{var J;const[e,r]=h(!1),s=b(!1),[o,d]=h(((J=window.SMBCommerceWidget)==null?void 0:J.widgetConfig)||null),[c,p]=h(null),[m,S]=h([]),[M,L]=h("bot_active"),[ee,z]=h(null),F=b(null),$=b(null),k=b(!1),[B,D]=h(null),[te,W]=h(0),l=b({id:0,at:0}),_=b({isOpen:e,conversationId:c,status:M,messages:m});_.current={isOpen:e,conversationId:c,status:M,messages:m},g(()=>{if(c===null)return;const t=fe(c);l.current=t.id>0?t:{id:C(m),at:Date.now()},t.id===0&&x(c,l.current.id,l.current.at),W(G(m,l.current.id))},[c]),g(()=>{c!==null&&(e||k.current)&&(l.current={id:C(m),at:Date.now()},x(c,l.current.id,l.current.at),W(0))},[e,m]),g(()=>{const t=window.setInterval(async()=>{const n=_.current;if(me({open:n.isOpen,conversationId:n.conversationId,status:n.status,lastActivityAt:l.current.at,visible:document.visibilityState==="visible",now:Date.now()}))try{const i=await R.pollMessages(n.conversationId,C(n.messages)||null);i.status&&L(i.status);const f=(i.messages||[]).filter(a=>Number(a.id)>C(_.current.messages));if(!f.length)return;const u=he(_.current.messages,f);S(u),l.current={...l.current,at:Date.now()},x(n.conversationId,l.current.id,l.current.at),W(G(u,l.current.id))}catch{}},ue);return()=>window.clearInterval(t)},[]);const P=()=>{Q().then(D).catch(()=>{})},U=document.body.classList,T=U.contains("woocommerce-checkout")||U.contains("woocommerce-cart");g(()=>{const t=window.visualViewport,n=F.current;if(!t||!n)return;let i=0;const f=()=>{if(i=0,!(window.innerWidth>document.documentElement.clientWidth+1)||n.classList.contains("is-open")){n.style.left=n.style.top=n.style.right=n.style.bottom="";return}const v=12,I=n.classList.contains("is-commerce-page")&&t.width<768?96:v;n.style.right="auto",n.style.bottom="auto",n.style.left=Math.max(t.offsetLeft+v,t.offsetLeft+t.width-n.offsetWidth-v)+"px",n.style.top=Math.max(t.offsetTop+v,t.offsetTop+t.height-n.offsetHeight-I)+"px"},u=()=>{i||(i=requestAnimationFrame(f))};t.addEventListener("resize",u),t.addEventListener("scroll",u),window.addEventListener("resize",u);const a=typeof ResizeObserver<"u"?new ResizeObserver(u):null;return a==null||a.observe(n),a==null||a.observe(document.documentElement),u(),()=>{t.removeEventListener("resize",u),t.removeEventListener("scroll",u),window.removeEventListener("resize",u),a==null||a.disconnect(),i&&cancelAnimationFrame(i)}},[]);const K=async()=>{if(c!==null)return!0;if(s.current)return!1;s.current=!0;try{const t=await R.bootstrap();return d(n=>({...n||{},...t.widget_config||{}})),p(t.conversation_id),S(t.messages||[]),L(t.status||"bot_active"),z(t.agent_name||null),!0}catch(t){return console.error("Failed to initialize Ninhle AI Sales Assistant widget",t),!1}finally{s.current=!1}};g(()=>{R.hasSession()&&(K(),P())},[]);const[V,H]=h(!1);g(()=>{if(!T||e){H(!1);return}if(typeof IntersectionObserver>"u")return;const t="#place_order, .wc-block-components-checkout-place-order-button, .checkout-button, .wc-block-cart__submit-button";let n=null;const i=new Map,f=()=>{var I;n==null||n.disconnect(),i.clear();const a=(I=$.current)==null?void 0:I.getBoundingClientRect();if(!a)return;const E=N=>`${-Math.max(0,Math.round(N))}px`,v=document.documentElement.clientWidth;n=new IntersectionObserver(N=>{N.forEach(Y=>i.set(Y.target,Y.isIntersecting)),H(Array.from(i.values()).some(Boolean))},{rootMargin:`${E(a.top-8)} ${E(v-a.right)} ${E(window.innerHeight-a.bottom-8)} ${E(a.left)}`}),document.querySelectorAll(t).forEach(N=>n.observe(N))};f(),window.addEventListener("resize",f);const u=window.setInterval(f,2e3);return()=>{n==null||n.disconnect(),window.removeEventListener("resize",f),clearInterval(u)}},[e]),g(()=>{var t;!e&&k.current&&((t=$.current)==null||t.focus()),k.current=e},[e]);const ne=async()=>{let t;try{t=await Q()}catch(n){console.error("Failed to load the Ninhle AI Sales Assistant chat window",n);return}D(t),await K()&&r(!0)},y=o!=null&&o.fontFamily&&o.fontFamily!=="system"?o.fontFamily:"";g(()=>{if(y){const t=y,n=`smb-commerce-font-${t.replace(/\s+/g,"-")}`;if(!document.getElementById(n)){const i=document.createElement("link");i.id=n,i.rel="stylesheet",i.href=`https://fonts.googleapis.com/css2?family=${t.replace(/\s+/g,"+")}:wght@400;500;600&display=swap`,document.head.appendChild(i)}}},[y]);const j=(o==null?void 0:o.primaryColor)||(o==null?void 0:o.primary_color)||"#01696f",q={"--smb-primary":j,"--smb-on-primary":le(j)};return y&&(q.fontFamily=`'${y}', sans-serif`),w("div",{ref:F,className:`smb-commerce-widget-container ${e?"is-open":""} ${T?"is-commerce-page":""} ${V?"is-dodging":""}`,style:q,"aria-hidden":V?"true":void 0,children:e&&c!==null&&B?w(B.ChatWindow,{onClose:()=>r(!1),config:o,conversationId:c,initialMessages:m,initialStatus:M,initialAgentName:ee,onSnapshot:t=>{S(t.messages),L(t.status),z(t.agentName)}}):w(oe,{onClick:ne,onIntent:P,config:o,buttonRef:$,unread:te})})},X=document.getElementById("smb-commerce-widget-root");X&&se(w(we,{}),X);
  • ninhle-ai-sales-assistant/trunk/assets/dist/widget.css

    r3761175 r3765552  
    1 .smb-commerce-widget-container{--smb-bg: #ffffff;--smb-bg-soft: #f1f3f6;--smb-text: #1f2328;--smb-text-soft: #555b66;--smb-radius: 20px;--smb-shadow: 0 16px 40px rgba(15, 23, 42, .2), 0 2px 8px rgba(15, 23, 42, .08);--smb-focus: #2563eb;position:fixed;right:20px;bottom:20px;z-index:999999;min-width:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.45;color:var(--smb-text);text-align:left}.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{box-sizing:border-box!important}.smb-commerce-widget-container button,.smb-commerce-widget-container input,.smb-commerce-widget-container textarea{margin:0;font:inherit;letter-spacing:normal;text-transform:none}.smb-commerce-widget-container button,.smb-commerce-widget-container a{border:0;background:none;box-shadow:none;color:inherit;cursor:pointer;padding:0;min-height:0;text-decoration:none;line-height:1.2}.smb-commerce-widget-container button:disabled{cursor:default;opacity:.45}.smb-commerce-widget-container :focus-visible:not(textarea){outline:2px solid var(--smb-focus)!important;outline-offset:2px}.smb-commerce-widget-container h2,.smb-commerce-widget-container h3,.smb-commerce-widget-container p{margin:0;padding:0;font-family:inherit;letter-spacing:normal}.smb-commerce-widget-container .smb-launcher{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary);box-shadow:0 6px 20px #0f172a3d;transition:transform .18s ease}.smb-commerce-widget-container .smb-launcher:hover{transform:scale(1.06)}.smb-commerce-widget-container .smb-launcher.has-logo{overflow:hidden}.smb-launcher img{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%}.smb-chat{width:380px;height:min(620px,calc(100vh - 40px));display:flex;flex-direction:column;overflow:hidden;background:var(--smb-bg);border-radius:var(--smb-radius);box-shadow:var(--smb-shadow);animation:smb-in .18s ease-out}.smb-chat.is-expanded{width:min(720px,calc(100vw - 40px));height:min(840px,calc(100vh - 40px))}@keyframes smb-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.smb-chat-header{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 10px 12px 16px;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-chat-identity{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.smb-chat-logo{flex-shrink:0;width:36px;height:36px;border-radius:50%;object-fit:cover;background:#ffffff38}.smb-chat-titles{min-width:0}.smb-commerce-widget-container .smb-chat-title{font-size:15px;font-weight:600;line-height:1.25;color:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.smb-commerce-widget-container .smb-chat-status{display:flex;align-items:center;gap:6px;font-size:12px;color:inherit;opacity:.92}.smb-status-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;background:#22c55e}.smb-status-dot.is-human_active{background:#f59e0b}.smb-status-dot.is-agent_active{background:#60a5fa}.smb-status-dot.is-closed{background:#9ca3af}.smb-chat-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.smb-commerce-widget-container .smb-icon-btn{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .18s ease}.smb-chat-header .smb-icon-btn:hover{background:#ffffff2e}.smb-icon-btn svg{display:block;pointer-events:none}.smb-menu-wrap{position:relative}.smb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:2;min-width:210px;padding:6px;background:var(--smb-bg);color:var(--smb-text);border-radius:14px;box-shadow:var(--smb-shadow)}.smb-commerce-widget-container .smb-menu button{display:block;width:100%;padding:10px 12px;border-radius:10px;text-align:left;font-size:14px}.smb-commerce-widget-container .smb-menu button:hover{background:var(--smb-bg-soft)}.smb-chat-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px;padding:16px 14px;background:var(--smb-bg)}.smb-commerce-widget-container .smb-ai-notice{align-self:center;max-width:92%;padding:6px 12px;border-radius:12px;background:var(--smb-bg-soft);color:var(--smb-text-soft);font-size:12px;text-align:center}.smb-msg{display:flex;max-width:86%;animation:smb-in .18s ease-out}.smb-msg.is-user{align-self:flex-end}.smb-msg.is-bot,.smb-msg.is-agent{align-self:flex-start}.smb-bubble{padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.is-user .smb-bubble{background:var(--smb-primary);color:var(--smb-on-primary);border-bottom-right-radius:6px}.is-bot .smb-bubble{background:var(--smb-bg-soft);color:var(--smb-text);border-bottom-left-radius:6px}.is-agent .smb-bubble{background:#e8efff;color:var(--smb-text);border-bottom-left-radius:6px}.smb-commerce-widget-container .message-link{color:#1d4ed8;text-decoration:underline;overflow-wrap:anywhere}.smb-commerce-widget-container .is-user .message-link{color:inherit}.smb-commerce-widget-container .message-image{display:block;max-width:100%;max-height:280px;width:auto;height:auto;margin:4px 0;border-radius:12px;object-fit:contain}.smb-commerce-widget-container .smb-md{white-space:normal}.smb-commerce-widget-container .smb-md>*+*{margin-top:8px}.smb-commerce-widget-container .smb-md p,.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol,.smb-commerce-widget-container .smb-md li,.smb-commerce-widget-container .smb-md blockquote,.smb-commerce-widget-container .smb-md pre,.smb-commerce-widget-container .smb-md table{margin:0;padding:0;border:0;font-size:inherit;line-height:inherit;color:inherit;background:none}.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol{padding-left:20px;list-style-position:outside}.smb-commerce-widget-container .smb-md ul{list-style-type:disc}.smb-commerce-widget-container .smb-md ul ul{list-style-type:circle}.smb-commerce-widget-container .smb-md ol{list-style-type:decimal}.smb-commerce-widget-container .smb-md li{display:list-item}.smb-commerce-widget-container .smb-md li+li,.smb-commerce-widget-container .smb-md li>ul,.smb-commerce-widget-container .smb-md li>ol{margin-top:4px}.smb-commerce-widget-container .smb-md strong{font-weight:700}.smb-commerce-widget-container .smb-md em{font-style:italic}.smb-commerce-widget-container .smb-md .smb-md-h{font-size:15px}.smb-commerce-widget-container .smb-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:#0f172a12;padding:1px 4px;border-radius:4px}.smb-commerce-widget-container .smb-md pre{padding:8px 10px;border-radius:8px;background:#0f172a12;overflow-x:auto;white-space:pre}.smb-commerce-widget-container .smb-md pre code{background:none;padding:0}.smb-commerce-widget-container .smb-md blockquote{padding-left:10px;border-left:3px solid rgba(15,23,42,.18);color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-md hr{height:0;margin:10px 0;border:0;border-top:1px solid rgba(15,23,42,.12)}.smb-commerce-widget-container .smb-md-table{overflow-x:auto}.smb-commerce-widget-container .smb-md table{border-collapse:collapse;width:100%;font-size:13px}.smb-commerce-widget-container .smb-md th,.smb-commerce-widget-container .smb-md td{padding:4px 8px;border:1px solid rgba(15,23,42,.12);text-align:left;vertical-align:top}.smb-commerce-widget-container .smb-md th{font-weight:700;background:#0f172a0a}.smb-typing{display:flex;align-items:center;gap:10px;color:var(--smb-text-soft);font-size:13px}.smb-dots{display:inline-flex;gap:4px}.smb-dots span{width:6px;height:6px;border-radius:50%;background:currentColor;animation:smb-dot 1.2s infinite ease-in-out both}.smb-dots span:nth-child(2){animation-delay:.15s}.smb-dots span:nth-child(3){animation-delay:.3s}@keyframes smb-dot{0%,80%,to{opacity:.25}40%{opacity:1}}.smb-card{padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-card p{margin-bottom:10px;font-weight:600}.smb-commerce-widget-container .smb-card-note{margin:0;font-weight:400;text-align:center;color:var(--smb-text-soft)}.smb-card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.smb-commerce-widget-container .smb-primary-btn,.smb-commerce-widget-container .smb-soft-btn{min-height:36px;padding:8px 16px;border-radius:999px;font-weight:600}.smb-commerce-widget-container .smb-primary-btn{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-soft-btn{background:var(--smb-bg);color:var(--smb-text)}.smb-commerce-widget-container .smb-text-btn{padding:8px;color:var(--smb-text-soft);text-decoration:underline}.smb-chips{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.smb-commerce-widget-container .smb-chip{padding:8px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;text-align:left;transition:background-color .18s ease}.smb-commerce-widget-container .smb-chip:hover{background:#e4e7ec}.smb-lead-form{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-lead-form h3{font-size:14px;font-weight:600}.smb-lead-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-lead-form input{width:100%;padding:10px 12px;border:0;border-radius:10px;background:var(--smb-bg);color:var(--smb-text);font-size:14px;box-shadow:none}.smb-commerce-widget-container .smb-error{margin-bottom:8px;padding:6px 10px;border-radius:10px;background:#fef3f2;color:#b42318;font-size:13px}.smb-chat-footer{flex-shrink:0;padding:8px 12px 10px;background:var(--smb-bg)}.smb-composer{display:flex;align-items:flex-end;gap:4px;padding:4px;border-radius:24px;background:var(--smb-bg-soft);transition:box-shadow .18s ease}.smb-composer:focus-within{box-shadow:0 0 0 2px var(--smb-focus)}.smb-composer .smb-icon-btn{color:var(--smb-text-soft)}.smb-composer .smb-icon-btn:hover:not(:disabled){background:#e4e7ec}.smb-commerce-widget-container .smb-composer .smb-mic{flex-shrink:0}.smb-commerce-widget-container .smb-composer .smb-mic.is-listening{color:#b42318;background:#fee4e2;animation:smb-pulse 1.4s ease-out infinite}@keyframes smb-pulse{0%{box-shadow:0 0 #b4231859}to{box-shadow:0 0 0 10px #b4231800}}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus{flex:1;min-width:0;min-height:36px;max-height:120px;padding:8px 4px;border:0;border-radius:0;outline:none;background:transparent;box-shadow:none;resize:none;color:var(--smb-text);font-size:14px;line-height:20px}.smb-composer textarea::placeholder{color:var(--smb-text-soft);opacity:1}.smb-commerce-widget-container .smb-send-btn{flex-shrink:0;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-footer-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.smb-footer-row:empty{display:none}.smb-commerce-widget-container .smb-human-btn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;font-weight:600}.smb-commerce-widget-container .smb-human-btn:hover:not(:disabled){background:#e4e7ec}.smb-channels{display:flex;gap:6px;margin-left:auto}.smb-commerce-widget-container .smb-channel{width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#fff;background:#374151}.smb-commerce-widget-container .smb-channel.is-whatsapp{background:#128c7e}.smb-commerce-widget-container .smb-channel.is-messenger{background:#0866ff}.smb-commerce-widget-container .smb-channel.is-telegram{background:#1c7fbf}.smb-commerce-widget-container .smb-channel.is-zalo{background:#0068ff}.smb-commerce-widget-container .smb-slogan{margin-top:6px;text-align:center;font-size:11px;color:var(--smb-text-soft)}.smb-sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.smb-commerce-widget-container.is-dodging .smb-launcher{opacity:0;pointer-events:none;visibility:hidden;transition:opacity .18s ease,visibility 0s linear .18s}@media (max-width: 767px){.smb-expand-btn{display:none!important}.smb-commerce-widget-container.is-commerce-page:not(.is-open){bottom:calc(96px + env(safe-area-inset-bottom,0px))}}@media (max-width: 480px){.smb-commerce-widget-container{right:12px;bottom:12px}.smb-commerce-widget-container.is-open{top:0;left:0;right:0;bottom:0}.smb-chat,.smb-chat.is-expanded{width:100vw;height:100vh;height:100dvh;border-radius:0;box-shadow:none}.smb-chat-header{padding-top:calc(12px + env(safe-area-inset-top,0px))}.smb-chat-footer{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px))}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus,.smb-commerce-widget-container .smb-lead-form input{font-size:16px}}@media (prefers-reduced-motion: reduce){.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{animation:none!important;transition:none!important}}
     1.smb-commerce-widget-container{--smb-bg: #ffffff;--smb-bg-soft: #f1f3f6;--smb-text: #1f2328;--smb-text-soft: #555b66;--smb-radius: 20px;--smb-shadow: 0 16px 40px rgba(15, 23, 42, .2), 0 2px 8px rgba(15, 23, 42, .08);--smb-focus: #2563eb;position:fixed;right:20px;bottom:20px;z-index:999999;min-width:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.45;color:var(--smb-text);text-align:left}.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{box-sizing:border-box!important}.smb-commerce-widget-container button,.smb-commerce-widget-container input,.smb-commerce-widget-container textarea{margin:0;font:inherit;letter-spacing:normal;text-transform:none}.smb-commerce-widget-container button,.smb-commerce-widget-container a{border:0;background:none;box-shadow:none;color:inherit;cursor:pointer;padding:0;min-height:0;text-decoration:none;line-height:1.2}.smb-commerce-widget-container button:disabled{cursor:default;opacity:.45}.smb-commerce-widget-container :focus-visible:not(textarea){outline:2px solid var(--smb-focus)!important;outline-offset:2px}.smb-commerce-widget-container h2,.smb-commerce-widget-container h3,.smb-commerce-widget-container p{margin:0;padding:0;font-family:inherit;letter-spacing:normal}.smb-commerce-widget-container .smb-launcher{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary);box-shadow:0 6px 20px #0f172a3d;transition:transform .18s ease}.smb-commerce-widget-container .smb-launcher:hover{transform:scale(1.06)}.smb-commerce-widget-container .smb-launcher{position:relative}.smb-commerce-widget-container .smb-unread{position:absolute;top:-4px;right:-4px;min-width:22px;height:22px;padding:0 6px;box-sizing:border-box;border-radius:11px;background:#d63638;color:#fff;border:2px solid #fff;font-size:12px;font-weight:600;line-height:18px;text-align:center}.smb-launcher img{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%}.smb-chat{width:380px;height:min(620px,calc(100vh - 40px));display:flex;flex-direction:column;overflow:hidden;background:var(--smb-bg);border-radius:var(--smb-radius);box-shadow:var(--smb-shadow);animation:smb-in .18s ease-out}.smb-chat.is-expanded{width:min(720px,calc(100vw - 40px));height:min(840px,calc(100vh - 40px))}@keyframes smb-in{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.smb-chat-header{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 10px 12px 16px;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-chat-identity{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.smb-chat-logo{flex-shrink:0;width:36px;height:36px;border-radius:50%;object-fit:cover;background:#ffffff38}.smb-chat-titles{min-width:0}.smb-commerce-widget-container .smb-chat-title{font-size:15px;font-weight:600;line-height:1.25;color:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.smb-commerce-widget-container .smb-chat-status{display:flex;align-items:center;gap:6px;font-size:12px;color:inherit;opacity:.92}.smb-status-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;background:#22c55e}.smb-status-dot.is-human_active{background:#f59e0b}.smb-status-dot.is-agent_active{background:#60a5fa}.smb-status-dot.is-closed{background:#9ca3af}.smb-chat-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.smb-commerce-widget-container .smb-icon-btn{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .18s ease}.smb-chat-header .smb-icon-btn:hover{background:#ffffff2e}.smb-icon-btn svg{display:block;pointer-events:none}.smb-menu-wrap{position:relative}.smb-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:2;min-width:210px;padding:6px;background:var(--smb-bg);color:var(--smb-text);border-radius:14px;box-shadow:var(--smb-shadow)}.smb-commerce-widget-container .smb-menu button{display:block;width:100%;padding:10px 12px;border-radius:10px;text-align:left;font-size:14px}.smb-commerce-widget-container .smb-menu button:hover{background:var(--smb-bg-soft)}.smb-chat-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px;padding:16px 14px;background:var(--smb-bg)}.smb-commerce-widget-container .smb-ai-notice{align-self:center;max-width:92%;padding:6px 12px;border-radius:12px;background:var(--smb-bg-soft);color:var(--smb-text-soft);font-size:12px;text-align:center}.smb-msg{display:flex;max-width:86%;animation:smb-in .18s ease-out}.smb-msg.is-user{align-self:flex-end}.smb-msg.is-bot,.smb-msg.is-agent{align-self:flex-start}.smb-bubble{padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.is-user .smb-bubble{background:var(--smb-primary);color:var(--smb-on-primary);border-bottom-right-radius:6px}.is-bot .smb-bubble{background:var(--smb-bg-soft);color:var(--smb-text);border-bottom-left-radius:6px}.is-agent .smb-bubble{background:#e8efff;color:var(--smb-text);border-bottom-left-radius:6px}.smb-msg.smb-msg-cards{max-width:100%;width:100%;margin-top:-2px;flex-direction:column}.smb-cards{position:relative;width:100%}.smb-cards-track{display:flex;gap:8px;margin:0;padding:2px 2px 6px;list-style:none;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch}.smb-cards-track::-webkit-scrollbar{display:none}.smb-pcard{flex:0 0 82%;max-width:260px;scroll-snap-align:start;display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid rgba(15,23,42,.1);border-radius:14px;background:var(--smb-bg);color:var(--smb-text)}.smb-cards.is-single .smb-pcard{flex-basis:100%;max-width:100%}.smb-pcard-img{flex:0 0 64px;width:64px;height:64px;border-radius:10px;object-fit:cover;background:var(--smb-bg-soft)}.smb-pcard-body{min-width:0;display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:1.35}.smb-commerce-widget-container .smb-pcard-name{color:var(--smb-text);font-weight:600;text-decoration:none;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.smb-commerce-widget-container .smb-pcard-name:hover{text-decoration:underline}.smb-pcard-price{margin:0;font-weight:600}.smb-pcard-price del{margin-right:6px;color:var(--smb-text-soft);font-weight:400}.smb-pcard-stock{margin:0;font-size:12px;color:#15803d}.smb-pcard-stock.is-outofstock{color:#b42318}.smb-pcard-stock.is-onbackorder{color:#b45309}.smb-pcard-at{margin:0;font-size:11px;color:var(--smb-text-soft)}.smb-pcard-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:wrap}.smb-commerce-widget-container .smb-pcard-btn{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;border-radius:999px;border:1px solid var(--smb-primary);color:var(--smb-primary);background:transparent;font-size:13px;font-weight:600;text-decoration:none;cursor:pointer}.smb-commerce-widget-container .smb-pcard-btn:hover{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-cards-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:1;width:30px;height:30px;border-radius:50%;border:0;cursor:pointer;background:var(--smb-bg);color:var(--smb-text);border:1px solid rgba(15,23,42,.12);box-shadow:0 2px 8px #0f172a2e;font-size:20px;font-weight:600;line-height:1}.smb-commerce-widget-container .smb-cards-nav.is-prev{left:-4px}.smb-commerce-widget-container .smb-cards-nav.is-next{right:-4px}.smb-pcard-btn:focus-visible,.smb-cards-nav:focus-visible,.smb-pcard-name:focus-visible{outline:2px solid var(--smb-focus);outline-offset:2px}@media (hover: none){.smb-commerce-widget-container .smb-cards-nav{display:none}}.smb-commerce-widget-container .smb-pcard-btn.is-primary{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-pcard-btn.is-primary:hover{filter:brightness(1.08)}.smb-commerce-widget-container .smb-pcard-btn[disabled]{opacity:.7;cursor:progress}.smb-commerce-widget-container .smb-pcard-link{color:var(--smb-primary);text-decoration:underline;font-size:13px}.smb-pcard-added{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px}.smb-pcard-added-text{color:#15803d;font-weight:600;font-size:13px}.smb-pcard-error{flex-basis:100%;color:#b42318;font-size:12px}.smb-commerce-widget-container .smb-cart-btn{position:relative}.smb-cart-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#fff;color:var(--smb-primary);font-size:10px;font-weight:700;line-height:16px;text-align:center}.smb-cart-panel{margin:0 0 8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg);border:1px solid rgba(15,23,42,.12);box-shadow:0 6px 18px #0f172a1f}.smb-cart-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.smb-cart-head h3{margin:0;font-size:15px;font-weight:700;color:var(--smb-text)}.smb-cart-head h3:focus{outline:none}.smb-cart-note{margin:4px 0;font-size:13px;color:var(--smb-text-soft)}.smb-cart-items{list-style:none;margin:0;padding:0}.smb-cart-item{display:flex;gap:8px;align-items:baseline;padding:6px 0;border-bottom:1px solid rgba(15,23,42,.08);font-size:13px}.smb-cart-name{flex:1;min-width:0;overflow-wrap:anywhere}.smb-cart-qty{color:var(--smb-text-soft);white-space:nowrap}.smb-cart-line{font-weight:600;white-space:nowrap}.smb-cart-subtotal{display:flex;justify-content:space-between;margin:8px 0;font-size:14px}.smb-cart-options{display:block;font-size:12px;color:var(--smb-text-soft)}.smb-ocards{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;width:100%}.smb-ocard{padding:10px 12px;border:1px solid rgba(15,23,42,.1);border-radius:14px;background:var(--smb-bg);color:var(--smb-text);font-size:13px;line-height:1.4}.smb-ocard-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:4px 8px}.smb-ocard-status{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600;background:var(--smb-bg-soft);color:var(--smb-text)}.smb-ocard-status.is-processing,.smb-ocard-status.is-on-hold{background:#fef3c7;color:#92400e}.smb-ocard-status.is-completed{background:#dcfce7;color:#166534}.smb-ocard-status.is-cancelled,.smb-ocard-status.is-failed,.smb-ocard-status.is-refunded{background:#fee2e2;color:#991b1b}.smb-ocard-meta{margin:4px 0 0;color:var(--smb-text-soft);font-size:12px}.smb-commerce-widget-container .smb-ocard-items{list-style:none;margin:6px 0 0;padding:0}.smb-ocard-items li{overflow-wrap:anywhere}.smb-ocard-more{color:var(--smb-text-soft)}.smb-ocard-track{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px;margin:8px 0 0}.smb-ocard-track code{font-size:12px;overflow-wrap:anywhere}.smb-ocard .smb-pcard-link{display:inline-block;margin-top:6px}.smb-cart-discount{display:flex;justify-content:space-between;margin:-4px 0 8px;font-size:13px;color:#15803d}.smb-cart-actions{display:flex;gap:8px;flex-wrap:wrap}.smb-commerce-widget-container .message-link{color:#1d4ed8;text-decoration:underline;overflow-wrap:anywhere}.smb-commerce-widget-container .is-user .message-link{color:inherit}.smb-commerce-widget-container .message-image{display:block;max-width:100%;max-height:280px;width:auto;height:auto;margin:4px 0;border-radius:12px;object-fit:contain}.smb-commerce-widget-container .smb-md{white-space:normal}.smb-commerce-widget-container .smb-md>*+*{margin-top:8px}.smb-commerce-widget-container .smb-md p,.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol,.smb-commerce-widget-container .smb-md li,.smb-commerce-widget-container .smb-md blockquote,.smb-commerce-widget-container .smb-md pre,.smb-commerce-widget-container .smb-md table{margin:0;padding:0;border:0;font-size:inherit;line-height:inherit;color:inherit;background:none}.smb-commerce-widget-container .smb-md ul,.smb-commerce-widget-container .smb-md ol{padding-left:20px;list-style-position:outside}.smb-commerce-widget-container .smb-md ul{list-style-type:disc}.smb-commerce-widget-container .smb-md ul ul{list-style-type:circle}.smb-commerce-widget-container .smb-md ol{list-style-type:decimal}.smb-commerce-widget-container .smb-md li{display:list-item}.smb-commerce-widget-container .smb-md li+li,.smb-commerce-widget-container .smb-md li>ul,.smb-commerce-widget-container .smb-md li>ol{margin-top:4px}.smb-commerce-widget-container .smb-md strong{font-weight:700}.smb-commerce-widget-container .smb-md em{font-style:italic}.smb-commerce-widget-container .smb-md .smb-md-h{font-size:15px}.smb-commerce-widget-container .smb-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:#0f172a12;padding:1px 4px;border-radius:4px}.smb-commerce-widget-container .smb-md pre{padding:8px 10px;border-radius:8px;background:#0f172a12;overflow-x:auto;white-space:pre}.smb-commerce-widget-container .smb-md pre code{background:none;padding:0}.smb-commerce-widget-container .smb-md blockquote{padding-left:10px;border-left:3px solid rgba(15,23,42,.18);color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-md hr{height:0;margin:10px 0;border:0;border-top:1px solid rgba(15,23,42,.12)}.smb-commerce-widget-container .smb-md-table{overflow-x:auto}.smb-commerce-widget-container .smb-md table{border-collapse:collapse;width:100%;font-size:13px}.smb-commerce-widget-container .smb-md th,.smb-commerce-widget-container .smb-md td{padding:4px 8px;border:1px solid rgba(15,23,42,.12);text-align:left;vertical-align:top}.smb-commerce-widget-container .smb-md th{font-weight:700;background:#0f172a0a}.smb-typing{display:flex;align-items:center;gap:10px;color:var(--smb-text-soft);font-size:13px}.smb-dots{display:inline-flex;gap:4px}.smb-dots span{width:6px;height:6px;border-radius:50%;background:currentColor;animation:smb-dot 1.2s infinite ease-in-out both}.smb-dots span:nth-child(2){animation-delay:.15s}.smb-dots span:nth-child(3){animation-delay:.3s}@keyframes smb-dot{0%,80%,to{opacity:.25}40%{opacity:1}}.smb-card{padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-card p{margin-bottom:10px;font-weight:600}.smb-commerce-widget-container .smb-card-note{margin:0;font-weight:400;text-align:center;color:var(--smb-text-soft)}.smb-card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.smb-commerce-widget-container .smb-primary-btn,.smb-commerce-widget-container .smb-soft-btn{min-height:36px;padding:8px 16px;border-radius:999px;font-weight:600}.smb-commerce-widget-container .smb-primary-btn{background:var(--smb-primary);color:var(--smb-on-primary)}.smb-commerce-widget-container .smb-soft-btn{background:var(--smb-bg);color:var(--smb-text)}.smb-commerce-widget-container .smb-text-btn{padding:8px;color:var(--smb-text-soft);text-decoration:underline}.smb-chips{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.smb-commerce-widget-container .smb-chip{padding:8px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;text-align:left;transition:background-color .18s ease}.smb-commerce-widget-container .smb-chip:hover{background:#e4e7ec}.smb-lead-form{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:16px;background:var(--smb-bg-soft)}.smb-commerce-widget-container .smb-lead-form h3{font-size:14px;font-weight:600}.smb-lead-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--smb-text-soft)}.smb-commerce-widget-container .smb-lead-form input{width:100%;padding:10px 12px;border:0;border-radius:10px;background:var(--smb-bg);color:var(--smb-text);font-size:14px;box-shadow:none}.smb-commerce-widget-container .smb-error{margin-bottom:8px;padding:6px 10px;border-radius:10px;background:#fef3f2;color:#b42318;font-size:13px}.smb-chat-footer{flex-shrink:0;padding:8px 12px 10px;background:var(--smb-bg)}.smb-composer{display:flex;align-items:flex-end;gap:4px;padding:4px;border-radius:24px;background:var(--smb-bg-soft);transition:box-shadow .18s ease}.smb-composer:focus-within{box-shadow:0 0 0 2px var(--smb-focus)}.smb-composer .smb-icon-btn{color:var(--smb-text-soft)}.smb-composer .smb-icon-btn:hover:not(:disabled){background:#e4e7ec}.smb-commerce-widget-container .smb-composer .smb-mic{flex-shrink:0}.smb-commerce-widget-container .smb-composer .smb-mic.is-listening{color:#b42318;background:#fee4e2;animation:smb-pulse 1.4s ease-out infinite}@keyframes smb-pulse{0%{box-shadow:0 0 #b4231859}to{box-shadow:0 0 0 10px #b4231800}}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus{flex:1;min-width:0;min-height:36px;max-height:120px;padding:8px 4px;border:0;border-radius:0;outline:none;background:transparent;box-shadow:none;resize:none;color:var(--smb-text);font-size:14px;line-height:20px}.smb-composer textarea::placeholder{color:var(--smb-text-soft);opacity:1}.smb-commerce-widget-container .smb-send-btn{flex-shrink:0;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--smb-primary);color:var(--smb-on-primary)}.smb-footer-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.smb-footer-row:empty{display:none}.smb-commerce-widget-container .smb-human-btn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;border-radius:999px;background:var(--smb-bg-soft);color:var(--smb-text);font-size:13px;font-weight:600}.smb-commerce-widget-container .smb-human-btn:hover:not(:disabled){background:#e4e7ec}.smb-channels{display:flex;gap:6px;margin-left:auto}.smb-commerce-widget-container .smb-channel{width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#fff;background:#374151}.smb-commerce-widget-container .smb-channel.is-whatsapp{background:#128c7e}.smb-commerce-widget-container .smb-channel.is-messenger{background:#0866ff}.smb-commerce-widget-container .smb-channel.is-telegram{background:#1c7fbf}.smb-commerce-widget-container .smb-channel.is-zalo{background:#0068ff}.smb-commerce-widget-container .smb-slogan{margin-top:6px;text-align:center;font-size:11px;color:var(--smb-text-soft)}.smb-sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.smb-commerce-widget-container.is-dodging .smb-launcher{opacity:0;pointer-events:none;visibility:hidden;transition:opacity .18s ease,visibility 0s linear .18s}@media (max-width: 767px){.smb-expand-btn{display:none!important}.smb-commerce-widget-container.is-commerce-page:not(.is-open){bottom:calc(96px + env(safe-area-inset-bottom,0px))}}@media (max-width: 480px){.smb-commerce-widget-container{right:12px;bottom:12px}.smb-commerce-widget-container.is-open{top:0;left:0;right:0;bottom:0}.smb-chat,.smb-chat.is-expanded{width:100vw;height:100vh;height:100dvh;border-radius:0;box-shadow:none}.smb-chat-header{padding-top:calc(12px + env(safe-area-inset-top,0px))}.smb-chat-footer{padding-bottom:calc(10px + env(safe-area-inset-bottom,0px))}.smb-commerce-widget-container .smb-composer textarea,.smb-commerce-widget-container .smb-composer textarea:focus,.smb-commerce-widget-container .smb-lead-form input{font-size:16px}}@media (prefers-reduced-motion: reduce){.smb-commerce-widget-container *,.smb-commerce-widget-container *:before,.smb-commerce-widget-container *:after{animation:none!important;transition:none!important}}
  • ninhle-ai-sales-assistant/trunk/languages/ninhle-ai-sales-assistant.pot

    r3761175 r3765552  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Ninhle AI Sales Assistant for WooCommerce 2.5.0\n"
     5"Project-Id-Version: Ninhle AI Sales Assistant – AI Chatbot for WooCommerce 2.6.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ninhle-ai-sales-assistant\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-23T06:16:45+00:00\n"
     12"POT-Creation-Date: 2026-10-26T03:45:21+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1717#. Plugin Name of the plugin
    1818#: ninhle-ai-sales-assistant.php
    19 msgid "Ninhle AI Sales Assistant for WooCommerce"
     19msgid "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce"
    2020msgstr ""
    2121
    2222#. Description of the plugin
    2323#: ninhle-ai-sales-assistant.php
    24 msgid "AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff."
     24msgid "AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in the chat, tracks orders and shows why shoppers don't buy."
    2525msgstr ""
    2626
    … …  
    3535msgstr ""
    3636
    37 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:26
     37#. translators: %s: discount percentage, e.g. "10".
     38#: premium/Commerce/Woo/CartActionService.php:376
     39#, php-format
     40msgid "%s%% off"
     41msgstr ""
     42
     43#. translators: %s: discount amount with currency.
     44#: premium/Commerce/Woo/CartActionService.php:381
     45#, php-format
     46msgid "%s off each item"
     47msgstr ""
     48
     49#. translators: %s: discount amount with currency.
     50#: premium/Commerce/Woo/CartActionService.php:384
     51#, php-format
     52msgid "%s off your order"
     53msgstr ""
     54
     55#: premium/Commerce/Woo/CartActionService.php:471
     56msgid "Quantity"
     57msgstr ""
     58
     59#: premium/Commerce/Woo/CartActionService.php:472
     60msgid "Decrease quantity"
     61msgstr ""
     62
     63#: premium/Commerce/Woo/CartActionService.php:473
     64msgid "Increase quantity"
     65msgstr ""
     66
     67#. translators: %s: product name.
     68#: premium/Commerce/Woo/CartActionService.php:475
     69#, php-format
     70msgid "Remove %s"
     71msgstr ""
     72
     73#: premium/Commerce/Woo/CartActionService.php:476
     74msgid "Remove"
     75msgstr ""
     76
     77#: premium/Commerce/Woo/CartActionService.php:477
     78msgid "Choose your options to add this to the cart."
     79msgstr ""
     80
     81#: premium/Commerce/Woo/CartActionService.php:478
     82msgid "sold out"
     83msgstr ""
     84
     85#: premium/Commerce/Woo/CartActionService.php:479
     86msgid "This combination is not available."
     87msgstr ""
     88
     89#. translators: %d: largest quantity that can be added.
     90#: premium/Commerce/Woo/CartActionService.php:481
     91#, php-format
     92msgid "Up to %d"
     93msgstr ""
     94
     95#: premium/Commerce/Woo/CartActionService.php:482
     96msgid "Updating your cart…"
     97msgstr ""
     98
     99#: premium/Commerce/Woo/CartActionService.php:483
     100msgid "Couldn't update your cart."
     101msgstr ""
     102
     103#: premium/Commerce/Woo/CartActionService.php:484
     104msgid "Apply"
     105msgstr ""
     106
     107#: premium/Commerce/Woo/CartActionService.php:485
     108msgid "Applying…"
     109msgstr ""
     110
     111#. translators: %s: coupon code.
     112#: premium/Commerce/Woo/CartActionService.php:487
     113#, php-format
     114msgid "Code %s applied"
     115msgstr ""
     116
     117#: premium/Commerce/Woo/CartActionService.php:488
     118msgid "Couldn't apply this code."
     119msgstr ""
     120
     121#. translators: %s: minimum order amount with currency.
     122#: premium/Commerce/Woo/CartActionService.php:490
     123#, php-format
     124msgid "Minimum order %s"
     125msgstr ""
     126
     127#: premium/Commerce/Woo/CartActionService.php:491
     128msgid "Discount codes"
     129msgstr ""
     130
     131#: premium/CRM/CrmStore.php:609
     132msgid "Customer ID"
     133msgstr ""
     134
     135#: premium/CRM/CrmStore.php:610
     136#: src/PHP/Privacy/PersonalData.php:249
     137#: src/PHP/REST/LeadController.php:145
     138#: src/PHP/Widget/WidgetLoader.php:94
     139msgid "Name"
     140msgstr ""
     141
     142#: premium/CRM/CrmStore.php:611
     143msgid "Verified"
     144msgstr ""
     145
     146#: premium/CRM/CrmStore.php:611
     147#: src/PHP/Widget/WidgetLoader.php:155
     148msgid "Yes"
     149msgstr ""
     150
     151#: premium/CRM/CrmStore.php:611
     152msgid "No"
     153msgstr ""
     154
     155#. translators: %s: contact type, for example "email" or "phone".
     156#: premium/CRM/CrmStore.php:616
     157#, php-format
     158msgid "Contact (%s)"
     159msgstr ""
     160
     161#: premium/CRM/CrmStore.php:620
     162msgid "Ninhle AI Sales Assistant customer profile"
     163msgstr ""
     164
     165#: premium/CRM/CrmStore.php:623
     166msgid "Ninhle AI Sales Assistant contact permissions"
     167msgstr ""
     168
     169#: premium/CRM/CrmStore.php:624
     170msgid "Purpose"
     171msgstr ""
     172
     173#: premium/CRM/CrmStore.php:625
     174#: premium/CRM/CrmStore.php:666
     175#: src/PHP/Privacy/PersonalData.php:296
     176msgid "Channel"
     177msgstr ""
     178
     179#: premium/CRM/CrmStore.php:626
     180#: premium/CRM/CrmStore.php:676
     181#: src/PHP/Privacy/PersonalData.php:252
     182#: src/PHP/REST/LeadController.php:148
     183msgid "Status"
     184msgstr ""
     185
     186#: premium/CRM/CrmStore.php:627
     187msgid "Source"
     188msgstr ""
     189
     190#: premium/CRM/CrmStore.php:628
     191msgid "Recorded"
     192msgstr ""
     193
     194#: premium/CRM/CrmStore.php:634
     195msgid "Ninhle AI Sales Assistant purchase opportunities"
     196msgstr ""
     197
     198#: premium/CRM/CrmStore.php:635
     199msgid "Need"
     200msgstr ""
     201
     202#: premium/CRM/CrmStore.php:636
     203msgid "Stage"
     204msgstr ""
     205
     206#: premium/CRM/CrmStore.php:637
     207msgid "Payment"
     208msgstr ""
     209
     210#: premium/CRM/CrmStore.php:638
     211#: src/PHP/Widget/WidgetLoader.php:118
     212msgid "Products"
     213msgstr ""
     214
     215#: premium/CRM/CrmStore.php:639
     216#: src/PHP/Admin/AdminMenu.php:215
     217msgid "Orders"
     218msgstr ""
     219
     220#: premium/CRM/CrmStore.php:640
     221#: premium/CRM/CrmStore.php:678
     222#: src/PHP/Privacy/PersonalData.php:254
     223#: src/PHP/REST/LeadController.php:150
     224msgid "Created"
     225msgstr ""
     226
     227#: premium/CRM/CrmStore.php:664
     228msgid "Ninhle AI Sales Assistant customer history"
     229msgstr ""
     230
     231#: premium/CRM/CrmStore.php:665
     232#: premium/CRM/CrmStore.php:675
     233msgid "Type"
     234msgstr ""
     235
     236#: premium/CRM/CrmStore.php:667
     237msgid "Content"
     238msgstr ""
     239
     240#: premium/CRM/CrmStore.php:668
     241#: src/PHP/Privacy/PersonalData.php:337
     242msgid "When"
     243msgstr ""
     244
     245#: premium/CRM/CrmStore.php:674
     246msgid "Ninhle AI Sales Assistant support and follow-up requests"
     247msgstr ""
     248
     249#: premium/CRM/CrmStore.php:677
     250msgid "Resolution"
     251msgstr ""
     252
     253#. translators: %d: number of chats
     254#: premium/Insights/InsightsDigest.php:66
     255#, php-format
     256msgid "%d chat with the assistant in the last 7 days."
     257msgid_plural "%d chats with the assistant in the last 7 days."
     258msgstr[0] ""
     259msgstr[1] ""
     260
     261#: premium/Insights/InsightsDigest.php:69
     262msgid "Questions the assistant could not answer:"
     263msgstr ""
     264
     265#: premium/Insights/InsightsDigest.php:72
     266#: premium/Insights/InsightsDigest.php:94
     267msgid "None this week."
     268msgstr ""
     269
     270#. translators: 1: customer question, 2: number of times asked
     271#: premium/Insights/InsightsDigest.php:76
     272#, php-format
     273msgid "- \"%1$s\" (%2$d×)"
     274msgstr ""
     275
     276#: premium/Insights/InsightsDigest.php:80
     277msgid "What customers looked for but could not get:"
     278msgstr ""
     279
     280#. translators: 1: product name, 2: number of times asked
     281#: premium/Insights/InsightsDigest.php:84
     282#, php-format
     283msgid "- %1$s: out of stock (%2$d×)"
     284msgstr ""
     285
     286#. translators: 1: search words, 2: number of searches
     287#: premium/Insights/InsightsDigest.php:88
     288#, php-format
     289msgid "- \"%1$s\": no matching product (%2$d×)"
     290msgstr ""
     291
     292#. translators: 1: reason, 2: percentage, 3: number of conversations labeled
     293#: premium/Insights/InsightsDigest.php:104
     294#, php-format
     295msgid "Most common reason shoppers did not buy: %1$s (%2$s%% of %3$d chats)."
     296msgstr ""
     297
     298#. translators: %s: amount in US dollars
     299#: premium/Insights/InsightsDigest.php:110
     300#, php-format
     301msgid "AI cost in the last 7 days: %s"
     302msgstr ""
     303
     304#. translators: %s: link to the Sales Insights tab
     305#: premium/Insights/InsightsDigest.php:113
     306#, php-format
     307msgid "Open Sales Insights: %s"
     308msgstr ""
     309
     310#: premium/Insights/InsightsDigest.php:114
     311msgid "You can turn this email off in Sales Insights > Settings."
     312msgstr ""
     313
     314#. translators: %s: site name
     315#: premium/Insights/InsightsDigest.php:118
     316#, php-format
     317msgid "[%s] Your weekly Sales Insights"
     318msgstr ""
     319
     320#: premium/Insights/InsightsDigest.php:125
     321msgid "price"
     322msgstr ""
     323
     324#: premium/Insights/InsightsDigest.php:126
     325msgid "shipping cost or time"
     326msgstr ""
     327
     328#: premium/Insights/InsightsDigest.php:127
     329msgid "unsure about size or fit"
     330msgstr ""
     331
     332#: premium/Insights/InsightsDigest.php:128
     333msgid "out of stock"
     334msgstr ""
     335
     336#: premium/Insights/InsightsDigest.php:129
     337msgid "missing information"
     338msgstr ""
     339
     340#: premium/Insights/InsightsDigest.php:130
     341msgid "waiting for a discount"
     342msgstr ""
     343
     344#: premium/Insights/InsightsDigest.php:131
     345msgid "just browsing"
     346msgstr ""
     347
     348#: premium/Insights/InsightsDigest.php:132
     349msgid "other"
     350msgstr ""
     351
     352#: premium/Integrations/TelegramService.php:77
     353msgid "None"
     354msgstr ""
     355
     356#. translators: %s: chat widget brand name.
     357#: premium/Integrations/TelegramService.php:79
     358#, php-format
     359msgid "New lead from %s"
     360msgstr ""
     361
     362#: premium/Integrations/TelegramService.php:80
     363msgid "Name:"
     364msgstr ""
     365
     366#: premium/Integrations/TelegramService.php:81
     367msgid "Email:"
     368msgstr ""
     369
     370#: premium/Integrations/TelegramService.php:82
     371msgid "Phone:"
     372msgstr ""
     373
     374#: premium/REST/TelegramController.php:55
     375#: src/PHP/Core/JobDispatcher.php:99
     376msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly."
     377msgstr ""
     378
     379#: premium/Webhooks/WebhookDispatcher.php:113
     380msgid "Outgoing webhooks are available in the Pro plan."
     381msgstr ""
     382
     383#: src/PHP/Abilities/AbilitiesService.php:57
     384msgid "Store catalog"
     385msgstr ""
     386
     387#: src/PHP/Abilities/AbilitiesService.php:58
     388msgid "Read-only access to the public product catalog and store policies."
     389msgstr ""
     390
     391#: src/PHP/Abilities/AbilitiesService.php:75
     392msgid "Search products"
     393msgstr ""
     394
     395#: src/PHP/Abilities/AbilitiesService.php:76
     396msgid "Searches the public product catalog of this store. Returns up to 10 products with price, currency, stock status, link and image."
     397msgstr ""
     398
     399#: src/PHP/Abilities/AbilitiesService.php:104
     400msgid "Get product details"
     401msgstr ""
     402
     403#: src/PHP/Abilities/AbilitiesService.php:105
     404msgid "Returns public details of one product: price, stock, description, categories, attributes and, for products with options, each option with its price and stock."
     405msgstr ""
     406
     407#: src/PHP/Abilities/AbilitiesService.php:120
     408msgid "Get store policies"
     409msgstr ""
     410
     411#: src/PHP/Abilities/AbilitiesService.php:121
     412msgid "Returns the store delivery, refund and warranty policies and the payment methods it accepts."
     413msgstr ""
     414
     415#: src/PHP/Abilities/AbilitiesService.php:153
     416msgid "Too many requests. Try again later."
     417msgstr ""
     418
     419#: src/PHP/Abilities/AbilitiesService.php:197
     420msgid "Product not found."
     421msgstr ""
     422
    38423#: src/PHP/Admin/AdminMenu.php:26
    39424msgid "Ninhle AI Sales Assistant"
    40425msgstr ""
    41426
    42 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:27
    43427#: src/PHP/Admin/AdminMenu.php:27
    44428msgid "Ninhle AI Sales"
    45429msgstr ""
    46430
    47 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:91
    48431#: src/PHP/Admin/AdminMenu.php:91
     432msgid "Sales Insights"
     433msgstr ""
     434
     435#: src/PHP/Admin/AdminMenu.php:92
     436msgid "What shoppers asked that the assistant could not answer, what they looked for but could not get, and why they did not buy. Contact details are hidden."
     437msgstr ""
     438
     439#. translators: %s: number of chats
     440#: src/PHP/Admin/AdminMenu.php:94
     441#, php-format
     442msgid "%s chats in this period"
     443msgstr ""
     444
     445#. translators: 1: number of chats so far, 2: number of chats needed
     446#: src/PHP/Admin/AdminMenu.php:96
     447#, php-format
     448msgid "Not enough data yet (%1$s of %2$s chats); the lists below are a first look."
     449msgstr ""
     450
     451#: src/PHP/Admin/AdminMenu.php:98
     452msgid "Questions the assistant could not answer"
     453msgstr ""
     454
     455#: src/PHP/Admin/AdminMenu.php:99
     456msgid "Similar questions are grouped. Add an answer to teach the assistant, or dismiss questions you do not want to answer. A question shows again if shoppers keep asking it after you added an answer."
     457msgstr ""
     458
     459#: src/PHP/Admin/AdminMenu.php:100
     460msgid "No unanswered questions in this period."
     461msgstr ""
     462
     463#: src/PHP/Admin/AdminMenu.php:101
     464#: src/PHP/Admin/AdminMenu.php:116
     465msgid "Question"
     466msgstr ""
     467
     468#: src/PHP/Admin/AdminMenu.php:102
     469msgid "Times asked"
     470msgstr ""
     471
     472#: src/PHP/Admin/AdminMenu.php:103
     473msgid "Last asked"
     474msgstr ""
     475
     476#: src/PHP/Admin/AdminMenu.php:104
     477msgid "Actions"
     478msgstr ""
     479
     480#. translators: %s: other ways the question was asked
     481#: src/PHP/Admin/AdminMenu.php:106
     482#, php-format
     483msgid "Also asked as: %s"
     484msgstr ""
     485
     486#: src/PHP/Admin/AdminMenu.php:107
     487msgid "shopper said it was not answered"
     488msgstr ""
     489
     490#: src/PHP/Admin/AdminMenu.php:108
     491msgid "information missing"
     492msgstr ""
     493
     494#: src/PHP/Admin/AdminMenu.php:109
     495msgid "no matching product"
     496msgstr ""
     497
     498#: src/PHP/Admin/AdminMenu.php:110
     499msgid "passed to your team"
     500msgstr ""
     501
     502#: src/PHP/Admin/AdminMenu.php:111
     503msgid "local-only mode"
     504msgstr ""
     505
     506#: src/PHP/Admin/AdminMenu.php:112
     507msgid "Add answer"
     508msgstr ""
     509
     510#: src/PHP/Admin/AdminMenu.php:113
     511msgid "Dismiss"
     512msgstr ""
     513
     514#: src/PHP/Admin/AdminMenu.php:114
     515msgid "Question dismissed."
     516msgstr ""
     517
     518#: src/PHP/Admin/AdminMenu.php:115
     519msgid "Open a chat"
     520msgstr ""
     521
     522#: src/PHP/Admin/AdminMenu.php:117
     523msgid "Answer"
     524msgstr ""
     525
     526#: src/PHP/Admin/AdminMenu.php:118
     527msgid "Where to save the answer"
     528msgstr ""
     529
     530#: src/PHP/Admin/AdminMenu.php:119
     531msgid "Add to Business information (the assistant uses it from the next reply)"
     532msgstr ""
     533
     534#: src/PHP/Admin/AdminMenu.php:120
     535msgid "Save as a note so I can update my policy pages"
     536msgstr ""
     537
     538#: src/PHP/Admin/AdminMenu.php:121
     539msgid "Save answer"
     540msgstr ""
     541
     542#: src/PHP/Admin/AdminMenu.php:122
     543#: src/PHP/Widget/WidgetLoader.php:81
     544msgid "Cancel"
     545msgstr ""
     546
     547#: src/PHP/Admin/AdminMenu.php:123
     548msgid "Answer added to Business information."
     549msgstr ""
     550
     551#: src/PHP/Admin/AdminMenu.php:124
     552msgid "Answer saved as a note."
     553msgstr ""
     554
     555#: src/PHP/Admin/AdminMenu.php:125
     556msgid "Could not save the answer. Enter both the question and the answer."
     557msgstr ""
     558
     559#: src/PHP/Admin/AdminMenu.php:126
     560msgid "Notes for your policy pages"
     561msgstr ""
     562
     563#: src/PHP/Admin/AdminMenu.php:127
     564msgid "Answers you chose to add to your own pages. The assistant does not read these notes."
     565msgstr ""
     566
     567#: src/PHP/Admin/AdminMenu.php:128
     568msgid "Delete"
     569msgstr ""
     570
     571#: src/PHP/Admin/AdminMenu.php:130
     572msgid "What shoppers could not find"
     573msgstr ""
     574
     575#: src/PHP/Admin/AdminMenu.php:131
     576msgid "Searches with no matching product"
     577msgstr ""
     578
     579#: src/PHP/Admin/AdminMenu.php:132
     580msgid "Wanted but out of stock"
     581msgstr ""
     582
     583#: src/PHP/Admin/AdminMenu.php:133
     584msgid "None in this period."
     585msgstr ""
     586
     587#: src/PHP/Admin/AdminMenu.php:134
     588msgid "Search your products"
     589msgstr ""
     590
     591#: src/PHP/Admin/AdminMenu.php:135
     592msgid "In stock now"
     593msgstr ""
     594
     595#: src/PHP/Admin/AdminMenu.php:136
     596msgid "Backorders allowed"
     597msgstr ""
     598
     599#: src/PHP/Admin/AdminMenu.php:137
     600msgid "Restock it, or allow backorders under Product data > Inventory."
     601msgstr ""
     602
     603#: src/PHP/Admin/AdminMenu.php:139
     604msgid "Why shoppers did not buy"
     605msgstr ""
     606
     607#: src/PHP/Admin/AdminMenu.php:140
     608msgid "Available in the Starter plan: each night an AI reads finished chats that did not lead to an order and sorts them by reason (price, shipping, size, stock, missing information and more), with example quotes."
     609msgstr ""
     610
     611#: src/PHP/Admin/AdminMenu.php:141
     612msgid "Turn on AI labeling below to see why shoppers did not buy."
     613msgstr ""
     614
     615#: src/PHP/Admin/AdminMenu.php:142
     616msgid "AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on."
     617msgstr ""
     618
     619#. translators: 1: chats labeled so far, 2: chats needed
     620#: src/PHP/Admin/AdminMenu.php:144
     621#, php-format
     622msgid "Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough."
     623msgstr ""
     624
     625#: src/PHP/Admin/AdminMenu.php:145
     626msgid "Price"
     627msgstr ""
     628
     629#: src/PHP/Admin/AdminMenu.php:146
     630msgid "Shipping cost or time"
     631msgstr ""
     632
     633#: src/PHP/Admin/AdminMenu.php:147
     634msgid "Unsure about size or fit"
     635msgstr ""
     636
     637#: src/PHP/Admin/AdminMenu.php:148
     638#: src/PHP/Widget/WidgetLoader.php:123
     639msgid "Out of stock"
     640msgstr ""
     641
     642#: src/PHP/Admin/AdminMenu.php:149
     643msgid "Missing information"
     644msgstr ""
     645
     646#: src/PHP/Admin/AdminMenu.php:150
     647msgid "Waiting for a discount"
     648msgstr ""
     649
     650#: src/PHP/Admin/AdminMenu.php:151
     651msgid "Just browsing"
     652msgstr ""
     653
     654#: src/PHP/Admin/AdminMenu.php:152
     655msgid "Other"
     656msgstr ""
     657
     658#: src/PHP/Admin/AdminMenu.php:153
     659msgid "See the unanswered questions"
     660msgstr ""
     661
     662#. translators: 1: AI provider and model, 2: amount spent this month, 3: monthly limit
     663#: src/PHP/Admin/AdminMenu.php:155
     664#, php-format
     665msgid "Model: %1$s · This month: %2$s of %3$s"
     666msgstr ""
     667
     668#. translators: %s: date
     669#: src/PHP/Admin/AdminMenu.php:157
     670#, php-format
     671msgid "Last run: %s"
     672msgstr ""
     673
     674#: src/PHP/Admin/AdminMenu.php:158
     675msgid "Stopped: the monthly limit for AI labeling was reached."
     676msgstr ""
     677
     678#: src/PHP/Admin/AdminMenu.php:159
     679msgid "Stopped: the overall AI budget was reached."
     680msgstr ""
     681
     682#: src/PHP/Admin/AdminMenu.php:160
     683msgid "Stopped: no AI provider with an API key is set up."
     684msgstr ""
     685
     686#: src/PHP/Admin/AdminMenu.php:161
     687msgid "Paused: 200 chats are labeled per night at most."
     688msgstr ""
     689
     690#: src/PHP/Admin/AdminMenu.php:163
     691msgid "Sales Insights settings"
     692msgstr ""
     693
     694#: src/PHP/Admin/AdminMenu.php:164
     695msgid "Label why shoppers did not buy, each night with AI"
     696msgstr ""
     697
     698#: src/PHP/Admin/AdminMenu.php:165
     699msgid "Runs at 2:00 site time on up to 200 finished chats that did not lead to an order, using your own AI provider key. Email addresses and phone numbers are hidden before anything is sent. Off by default."
     700msgstr ""
     701
     702#: src/PHP/Admin/AdminMenu.php:166
     703msgid "Monthly limit (USD)"
     704msgstr ""
     705
     706#: src/PHP/Admin/AdminMenu.php:167
     707msgid "Model"
     708msgstr ""
     709
     710#: src/PHP/Admin/AdminMenu.php:168
     711msgid "Cheapest available (recommended)"
     712msgstr ""
     713
     714#: src/PHP/Admin/AdminMenu.php:169
     715msgid "Same as the chat assistant"
     716msgstr ""
     717
     718#: src/PHP/Admin/AdminMenu.php:170
     719msgid "Email me a weekly summary"
     720msgstr ""
     721
     722#: src/PHP/Admin/AdminMenu.php:171
     723msgid "Every Monday morning to the site admin email: top unanswered questions, what shoppers could not find, the main reason they did not buy and the AI cost of the week. Not sent in a week without chats."
     724msgstr ""
     725
     726#: src/PHP/Admin/AdminMenu.php:178
    49727msgid "AI Value & Performance"
    50728msgstr ""
    51729
    52 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:92
    53 #: src/PHP/Admin/AdminMenu.php:92
     730#: src/PHP/Admin/AdminMenu.php:179
    54731msgid "What the assistant cost, the orders it helped with and how conversations ended. Figures start on the measurement start date and are never backfilled."
    55732msgstr ""
    56733
    57 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:93
    58 #: src/PHP/Admin/AdminMenu.php:93
     734#: src/PHP/Admin/AdminMenu.php:180
    59735msgid "Report period"
    60736msgstr ""
    61737
    62738#. translators: %s: number of days
    63 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:95
    64 #: src/PHP/Admin/AdminMenu.php:95
     739#: src/PHP/Admin/AdminMenu.php:182
    65740#, php-format
    66741msgid "Last %s days"
    … …  
    68743
    69744#. translators: %s: date the plugin started measuring
    70 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:97
    71 #: src/PHP/Admin/AdminMenu.php:97
     745#: src/PHP/Admin/AdminMenu.php:184
    72746#, php-format
    73747msgid "Measured since %s"
    74748msgstr ""
    75749
    76 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:98
    77 #: src/PHP/Admin/AdminMenu.php:98
     750#: src/PHP/Admin/AdminMenu.php:185
    78751msgid "Measurement starts after the database update to version 2.4.0."
    79752msgstr ""
    80753
    81 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:99
    82 #: src/PHP/Admin/AdminMenu.php:99
     754#: src/PHP/Admin/AdminMenu.php:186
    83755msgid "Loading the figures…"
    84756msgstr ""
    85757
    86 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:100
    87 #: src/PHP/Admin/AdminMenu.php:100
     758#: src/PHP/Admin/AdminMenu.php:187
    88759msgid "Could not load the figures. Please reload the page."
    89760msgstr ""
    90761
    91762#. translators: %s: sample size
    92 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:102
    93 #: src/PHP/Admin/AdminMenu.php:102
     763#: src/PHP/Admin/AdminMenu.php:189
    94764#, php-format
    95765msgid "n = %s"
    96766msgstr ""
    97767
    98 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:104
    99 #: src/PHP/Admin/AdminMenu.php:104
     768#: src/PHP/Admin/AdminMenu.php:191
    100769msgid "Cost and estimated comparison"
    101770msgstr ""
    102771
    103 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:105
    104 #: src/PHP/Admin/AdminMenu.php:105
     772#: src/PHP/Admin/AdminMenu.php:192
    105773msgid "AI cost recorded"
    106774msgstr ""
    107775
    108776#. translators: 1: cost from provider usage, 2: estimated cost
    109 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:107
    110 #: src/PHP/Admin/AdminMenu.php:107
     777#: src/PHP/Admin/AdminMenu.php:194
    111778#, php-format
    112779msgid "%1$s from provider usage, %2$s estimated"
    … …  
    114781
    115782#. translators: %s: number of AI requests
    116 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:109
    117 #: src/PHP/Admin/AdminMenu.php:109
     783#: src/PHP/Admin/AdminMenu.php:196
    118784#, php-format
    119785msgid "%s requests used a model with no known price and are counted as $0."
    120786msgstr ""
    121787
    122 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:110
    123 #: src/PHP/Admin/AdminMenu.php:110
     788#: src/PHP/Admin/AdminMenu.php:197
    124789msgid "Plugin cost, allocated per day"
    125790msgstr ""
    126791
    127 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:111
    128 #: src/PHP/Admin/AdminMenu.php:111
     792#: src/PHP/Admin/AdminMenu.php:198
    129793msgid "Other costs you entered"
    130794msgstr ""
    131795
    132 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:112
    133 #: src/PHP/Admin/AdminMenu.php:112
     796#: src/PHP/Admin/AdminMenu.php:199
    134797msgid "Total"
    135798msgstr ""
    136799
    137800#. translators: %s: name of the reference option, e.g. "SaaS AI agent"
    138 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:114
    139 #: src/PHP/Admin/AdminMenu.php:114
     801#: src/PHP/Admin/AdminMenu.php:201
    140802#, php-format
    141803msgid "Reference option: %s"
    … …  
    143805
    144806#. translators: 1: volume, 2: unit price
    145 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:116
    146 #: src/PHP/Admin/AdminMenu.php:116
     807#: src/PHP/Admin/AdminMenu.php:203
    147808#, php-format
    148809msgid "%1$s × %2$s"
    149810msgstr ""
    150811
    151 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:117
    152 #: src/PHP/Admin/AdminMenu.php:117
     812#: src/PHP/Admin/AdminMenu.php:204
    153813msgid "Estimated difference"
    154814msgstr ""
    155815
    156 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:118
    157 #: src/PHP/Admin/AdminMenu.php:118
     816#: src/PHP/Admin/AdminMenu.php:205
    158817msgid "Estimate based on the reference option you chose; not a verified saving."
    159818msgstr ""
    160819
    161 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:119
    162 #: src/PHP/Admin/AdminMenu.php:119
     820#: src/PHP/Admin/AdminMenu.php:206
    163821msgid "Add a reference option in Measurement settings to compare costs."
    164822msgstr ""
    165823
    166 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:120
    167 #: src/PHP/Admin/AdminMenu.php:120
     824#: src/PHP/Admin/AdminMenu.php:207
    168825msgid "AI cost covers model usage only. It is not the total cost of ownership."
    169826msgstr ""
    170827
    171 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:121
    172 #: src/PHP/Admin/AdminMenu.php:121
     828#: src/PHP/Admin/AdminMenu.php:208
    173829msgid "per AI conversation"
    174830msgstr ""
    175831
    176 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:122
    177 #: src/PHP/Admin/AdminMenu.php:122
     832#: src/PHP/Admin/AdminMenu.php:209
    178833msgid "per conversation the customer confirmed as resolved"
    179834msgstr ""
    180835
    181 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:123
    182 #: src/PHP/Admin/AdminMenu.php:123
     836#: src/PHP/Admin/AdminMenu.php:210
    183837msgid "per month"
    184838msgstr ""
    185839
    186840#. translators: %s: source of the reference price
    187 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:125
    188 #: src/PHP/Admin/AdminMenu.php:125
     841#: src/PHP/Admin/AdminMenu.php:212
    189842#, php-format
    190843msgid "Source: %s"
    191844msgstr ""
    192845
    193 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:127
    194 #: src/PHP/Admin/AdminMenu.php:127
     846#: src/PHP/Admin/AdminMenu.php:214
    195847msgid "AI-assisted orders"
    196848msgstr ""
    197849
    198 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:128
    199 #: premium/CRM/CrmStore.php:639
    200 #: src/PHP/Admin/AdminMenu.php:128
    201 msgid "Orders"
    202 msgstr ""
    203 
    204 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:129
    205 #: src/PHP/Admin/AdminMenu.php:129
     850#: src/PHP/Admin/AdminMenu.php:216
    206851msgid "AI-assisted does not mean the AI was the only reason for the order."
    207852msgstr ""
    208853
    209854#. translators: 1: attribution window such as "24 hours", 2: list of order statuses
    210 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:131
    211 #: src/PHP/Admin/AdminMenu.php:131
     855#: src/PHP/Admin/AdminMenu.php:218
    212856#, php-format
    213857msgid "Orders placed within %1$s after a qualifying chat interaction: a reply with products from your store, or a product or cart link clicked in the chat. Counted statuses: %2$s."
    214858msgstr ""
    215859
    216 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:132
    217 #: src/PHP/Admin/AdminMenu.php:132
     860#: src/PHP/Admin/AdminMenu.php:219
    218861msgid "AI-assisted revenue"
    219862msgstr ""
    220863
    221 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:133
    222 #: src/PHP/Admin/AdminMenu.php:133
     864#: src/PHP/Admin/AdminMenu.php:220
    223865msgid "Goods value after discounts, excluding tax and shipping. Partial refunds are deducted; fully refunded, cancelled and failed orders are excluded. Shown per currency, never converted."
    224866msgstr ""
    225867
    226 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:134
    227 #: src/PHP/Admin/AdminMenu.php:134
     868#: src/PHP/Admin/AdminMenu.php:221
    228869msgid "Revenue per currency is included in the Starter and Pro plans."
    229870msgstr ""
    230871
    231 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:135
    232 #: src/PHP/Admin/AdminMenu.php:135
     872#: src/PHP/Admin/AdminMenu.php:222
    233873msgid "No AI-assisted revenue in this period."
    234874msgstr ""
    235875
    236 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:137
    237 #: src/PHP/Admin/AdminMenu.php:137
     876#: src/PHP/Admin/AdminMenu.php:224
    238877msgid "Conversation outcomes"
    239878msgstr ""
    240879
    241 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:138
    242 #: src/PHP/Admin/AdminMenu.php:138
     880#: src/PHP/Admin/AdminMenu.php:225
    243881msgid "Finished conversations"
    244882msgstr ""
    245883
    246884#. translators: 1: finished conversations so far, 2: number needed
    247 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:140
    248 #: src/PHP/Admin/AdminMenu.php:140
     885#: src/PHP/Admin/AdminMenu.php:227
    249886#, php-format
    250887msgid "Not enough data yet: %1$s of %2$s finished conversations needed before rates are shown."
    251888msgstr ""
    252889
    253 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:141
    254 #: src/PHP/Admin/AdminMenu.php:141
     890#: src/PHP/Admin/AdminMenu.php:228
    255891msgid "Resolved, confirmed by the customer"
    256892msgstr ""
    257893
    258 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:142
    259 #: src/PHP/Admin/AdminMenu.php:142
     894#: src/PHP/Admin/AdminMenu.php:229
    260895msgid "Probably resolved: the customer clicked a product, added to cart or ordered"
    261896msgstr ""
    262897
    263 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:143
    264 #: src/PHP/Admin/AdminMenu.php:143
     898#: src/PHP/Admin/AdminMenu.php:230
    265899msgid "Handed off to staff"
    266900msgstr ""
    267901
    268 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:144
    269 #: src/PHP/Admin/AdminMenu.php:144
     902#: src/PHP/Admin/AdminMenu.php:231
    270903msgid "Unresolved, left before an answer, or unknown"
    271904msgstr ""
    272905
    273906#. translators: 1: lower bound, 2: upper bound of a 95% confidence interval, in percent
    274 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:146
    275 #: src/PHP/Admin/AdminMenu.php:146
     907#: src/PHP/Admin/AdminMenu.php:233
    276908#, php-format
    277909msgid "95%% range: %1$s–%2$s%%"
    278910msgstr ""
    279911
    280 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:147
    281 #: src/PHP/Admin/AdminMenu.php:147
     912#: src/PHP/Admin/AdminMenu.php:234
    282913msgid "\"Not handed off\" does not mean \"resolved\". A conversation is finished after 30 minutes without a new message; left-before-an-answer and unknown are never counted as success."
    283914msgstr ""
    284915
    285 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:148
    286 #: src/PHP/Admin/AdminMenu.php:148
     916#: src/PHP/Admin/AdminMenu.php:235
    287917msgid "Handoffs by source"
    288918msgstr ""
    289919
    290 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:149
    291 #: src/PHP/Admin/AdminMenu.php:149
     920#: src/PHP/Admin/AdminMenu.php:236
    292921msgid "AI"
    293922msgstr ""
    294923
    295 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:150
    296 #: src/PHP/Admin/AdminMenu.php:150
     924#: src/PHP/Admin/AdminMenu.php:237
    297925msgid "Automation rule"
    298926msgstr ""
    299927
    300 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:151
    301 #: src/PHP/Admin/AdminMenu.php:151
     928#: src/PHP/Admin/AdminMenu.php:238
    302929msgid "Customer button"
    303930msgstr ""
    304931
    305 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:152
    306 #: src/PHP/Admin/AdminMenu.php:152
     932#: src/PHP/Admin/AdminMenu.php:239
    307933msgid "Staff took over"
    308934msgstr ""
    309935
    310 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:153
    311 #: src/PHP/Admin/AdminMenu.php:153
     936#: src/PHP/Admin/AdminMenu.php:240
    312937msgid "AI budget reached"
    313938msgstr ""
    314939
    315 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:155
    316 #: src/PHP/Admin/AdminMenu.php:155
     940#: src/PHP/Admin/AdminMenu.php:242
    317941msgid "Measurement settings"
    318942msgstr ""
    319943
    320 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:156
    321 #: src/PHP/Admin/AdminMenu.php:156
     944#: src/PHP/Admin/AdminMenu.php:243
    322945msgid "Reference option name"
    323946msgstr ""
    324947
    325 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:157
    326 #: src/PHP/Admin/AdminMenu.php:157
     948#: src/PHP/Admin/AdminMenu.php:244
    327949msgid "Pricing unit"
    328950msgstr ""
    329951
    330 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:158
    331 #: src/PHP/Admin/AdminMenu.php:158
     952#: src/PHP/Admin/AdminMenu.php:245
    332953msgid "Price (USD)"
    333954msgstr ""
    334955
    335 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:159
    336 #: src/PHP/Admin/AdminMenu.php:159
     956#: src/PHP/Admin/AdminMenu.php:246
    337957msgid "Where the price comes from"
    338958msgstr ""
    339959
    340 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:160
    341 #: src/PHP/Admin/AdminMenu.php:160
     960#: src/PHP/Admin/AdminMenu.php:247
    342961msgid "Price date"
    343962msgstr ""
    344963
    345 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:161
    346 #: src/PHP/Admin/AdminMenu.php:161
     964#: src/PHP/Admin/AdminMenu.php:248
    347965msgid "Enter the price of an alternative you are considering. Leave the price at 0 to show costs only."
    348966msgstr ""
    349967
    350 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:162
    351 #: src/PHP/Admin/AdminMenu.php:162
     968#: src/PHP/Admin/AdminMenu.php:249
    352969msgid "Plugin cost per month (USD)"
    353970msgstr ""
    354971
    355 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:163
    356 #: src/PHP/Admin/AdminMenu.php:163
     972#: src/PHP/Admin/AdminMenu.php:250
    357973msgid "Other costs per month (USD)"
    358974msgstr ""
    359975
    360 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:164
    361 #: src/PHP/Admin/AdminMenu.php:164
     976#: src/PHP/Admin/AdminMenu.php:251
    362977msgid "Attribution window"
    363978msgstr ""
    364979
    365 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:165
    366 #: src/PHP/Admin/AdminMenu.php:165
     980#: src/PHP/Admin/AdminMenu.php:252
    367981msgid "How long after a qualifying chat interaction an order still counts as AI-assisted."
    368982msgstr ""
    369983
    370 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:166
    371 #: src/PHP/Admin/AdminMenu.php:166
     984#: src/PHP/Admin/AdminMenu.php:253
    372985msgid "24 hours"
    373986msgstr ""
    374987
    375 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:167
    376 #: src/PHP/Admin/AdminMenu.php:167
     988#: src/PHP/Admin/AdminMenu.php:254
    377989msgid "72 hours"
    378990msgstr ""
    379991
    380 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:168
    381 #: src/PHP/Admin/AdminMenu.php:168
     992#: src/PHP/Admin/AdminMenu.php:255
    382993msgid "7 days"
    383994msgstr ""
    384995
    385 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:169
    386 #: src/PHP/Admin/AdminMenu.php:169
     996#: src/PHP/Admin/AdminMenu.php:256
    387997msgid "Order statuses that count"
    388998msgstr ""
    389999
    390 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:170
    391 #: src/PHP/Admin/AdminMenu.php:170
     1000#: src/PHP/Admin/AdminMenu.php:257
    3921001msgid "Processing"
    3931002msgstr ""
    3941003
    395 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:171
    396 #: src/PHP/Admin/AdminMenu.php:171
     1004#: src/PHP/Admin/AdminMenu.php:258
    3971005msgid "Completed"
    3981006msgstr ""
    3991007
    400 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:172
    401 #: src/PHP/Admin/AdminMenu.php:172
     1008#: src/PHP/Admin/AdminMenu.php:259
    4021009msgid "On hold"
    4031010msgstr ""
    4041011
    405 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:173
    406 #: src/PHP/Admin/AdminMenu.php:173
     1012#: src/PHP/Admin/AdminMenu.php:260
    4071013msgid "Save settings"
    4081014msgstr ""
    4091015
    410 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:174
    411 #: src/PHP/Admin/AdminMenu.php:174
     1016#: src/PHP/Admin/AdminMenu.php:261
    4121017msgid "Saving…"
    4131018msgstr ""
    4141019
    415 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:175
    416 #: src/PHP/Admin/AdminMenu.php:175
     1020#: src/PHP/Admin/AdminMenu.php:262
    4171021msgid "Measurement settings saved."
    4181022msgstr ""
    4191023
    420 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:176
    421 #: src/PHP/Admin/AdminMenu.php:176
     1024#: src/PHP/Admin/AdminMenu.php:263
    4221025msgid "Could not save the measurement settings."
    4231026msgstr ""
    4241027
    425 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:94
     1028#. translators: %s: number of shopper conversations the AI assistant answered in the last 90 days.
     1029#: src/PHP/Admin/ReviewPrompt.php:175
     1030#, php-format
     1031msgid "In the last 90 days, your AI sales assistant answered %s shopper conversation."
     1032msgid_plural "In the last 90 days, your AI sales assistant answered %s shopper conversations."
     1033msgstr[0] ""
     1034msgstr[1] ""
     1035
     1036#. translators: %s: number of orders placed after a chat with the AI assistant.
     1037#: src/PHP/Admin/ReviewPrompt.php:181
     1038#, php-format
     1039msgid "%s order was placed after a chat with it."
     1040msgid_plural "%s orders were placed after a chat with it."
     1041msgstr[0] ""
     1042msgstr[1] ""
     1043
     1044#: src/PHP/Admin/ReviewPrompt.php:192
     1045msgid "If it has been useful, would you share your honest experience in a review on WordPress.org? It helps other store owners decide whether it fits their shop."
     1046msgstr ""
     1047
     1048#: src/PHP/Admin/ReviewPrompt.php:194
     1049msgid "Rate on WordPress.org"
     1050msgstr ""
     1051
     1052#: src/PHP/Admin/ReviewPrompt.php:196
     1053msgid "Maybe later"
     1054msgstr ""
     1055
     1056#: src/PHP/Admin/ReviewPrompt.php:198
     1057msgid "Don't ask again"
     1058msgstr ""
     1059
     1060#: src/PHP/Admin/ReviewPrompt.php:200
     1061msgid "Need help? Ask on the support forum"
     1062msgstr ""
     1063
    4261064#: src/PHP/Admin/SecurityNotice.php:94
    4271065msgid "Ninhle AI Sales Assistant: rotate your API credentials"
    4281066msgstr ""
    4291067
    430 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:95
    4311068#: src/PHP/Admin/SecurityNotice.php:95
    4321069msgid "This site previously ran a version of Ninhle AI Sales Assistant that stored provider credentials in plain text in the database. Those values have now been encrypted, but they should be treated as already exposed — anything written in plain text also went into every database backup taken since."
    4331070msgstr ""
    4341071
    435 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:99
    4361072#: src/PHP/Admin/SecurityNotice.php:99
    4371073msgid "Create new credentials at each provider you use (OpenAI, Anthropic, Google Gemini, Telegram bot), revoke the old ones, and enter the new values in Ninhle AI Sales Assistant settings."
    4381074msgstr ""
    4391075
    440 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:104
    4411076#: src/PHP/Admin/SecurityNotice.php:104
    4421077msgid "Dismiss this notice"
    4431078msgstr ""
    4441079
    445 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:16
    446 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:17
    4471080#: src/PHP/Admin/SettingsPage.php:16
    4481081#: src/PHP/Admin/SettingsPage.php:17
    … …  
    4511084
    4521085#. translators: %s: commission rate, for example "30%"
    453 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:45
    4541086#: src/PHP/Admin/SettingsPage.php:45
    4551087#, php-format
    … …  
    4571089msgstr ""
    4581090
    459 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:68
    4601091#: src/PHP/AI/AIService.php:68
    4611092msgid ""
    … …  
    4651096msgstr ""
    4661097
    467 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:232
    468 #: src/PHP/AI/AIService.php:232
     1098#: src/PHP/AI/AIService.php:258
    4691099msgid "The assistant is running in local-only mode. Please contact us through the channels below for help."
    4701100msgstr ""
    4711101
    472 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:250
    473 #: src/PHP/AI/AIService.php:250
     1102#: src/PHP/AI/AIService.php:277
    4741103msgid "Hello! The AI assistant is still being set up. Please try again shortly, or contact us through the channels below."
    4751104msgstr ""
    4761105
    477 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:304
    478 #: src/PHP/AI/AIService.php:304
     1106#: src/PHP/AI/AIService.php:331
    4791107msgid "Sorry, the assistant is temporarily unavailable. Please try again in a moment, or choose \"Talk to a person\"."
    4801108msgstr ""
    4811109
    482 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:400
    483 #: src/PHP/AI/AIService.php:400
    484 msgid "I am still looking this up for you, one moment please."
    485 msgstr ""
    486 
    487 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:406
    488 #: src/PHP/AI/AIService.php:406
     1110#: src/PHP/AI/AIService.php:455
     1111msgid "Sorry, I could not finish looking that up. Please ask me again in a moment."
     1112msgstr ""
     1113
     1114#: src/PHP/AI/AIService.php:461
    4891115msgid "Sorry, I could not put an answer together just now. Could you ask that again in a different way?"
    4901116msgstr ""
    4911117
    492 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:417
    493 #: src/PHP/AI/AIService.php:417
     1118#: src/PHP/AI/AIService.php:472
    4941119msgid "I understand. Let me bring in a member of our team to help you directly. Please wait a moment."
    4951120msgstr ""
    4961121
    497 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:418
    498 #: src/PHP/AI/AIService.php:418
     1122#: src/PHP/AI/AIService.php:473
    4991123msgid "We are connecting you with a manager now. Please stay in the chat."
    5001124msgstr ""
    5011125
    502 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:419
    503 #: src/PHP/AI/AIService.php:419
     1126#: src/PHP/AI/AIService.php:474
    5041127msgid "Your request is being escalated urgently to the responsible team. Please do not send any more personal information here."
    5051128msgstr ""
    5061129
    507 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:69
    5081130#: src/PHP/Commerce/LocalHandler.php:69
    5091131msgid "Hello! How can I help you today?"
    … …  
    5111133
    5121134#. translators: %s is the store or brand name.
    513 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:74
    5141135#: src/PHP/Commerce/LocalHandler.php:74
    5151136#, php-format
    … …  
    5171138msgstr ""
    5181139
    519 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:80
    5201140#: src/PHP/Commerce/LocalHandler.php:80
    5211141msgid "You are welcome. Just message me if you need anything else."
    5221142msgstr ""
    5231143
    524 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:123
    5251144#: src/PHP/Commerce/LocalHandler.php:123
    5261145msgid "Your cart is empty. Tell me which product you want and I will send you the link."
    … …  
    5281147
    5291148#. translators: %s is the checkout page URL.
    530 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:135
    5311149#: src/PHP/Commerce/LocalHandler.php:135
    5321150#, php-format
    … …  
    5351153
    5361154#. translators: %s is the cart page URL.
    537 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:155
    5381155#: src/PHP/Commerce/LocalHandler.php:155
    5391156#, php-format
    … …  
    5411158msgstr ""
    5421159
    543 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:163
    5441160#: src/PHP/Commerce/LocalHandler.php:163
     1161#: src/PHP/Widget/WidgetLoader.php:142
    5451162msgid "Your cart is empty."
    5461163msgstr ""
    5471164
    5481165#. translators: 1: list of cart lines, one per line. 2: checkout URL.
    549 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:173
    5501166#: src/PHP/Commerce/LocalHandler.php:173
    5511167#, php-format
    … …  
    5581174
    5591175#. translators: 1: order number. 2: order status. 3: date the order was placed.
    560 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:214
    561 #: src/PHP/Commerce/LocalHandler.php:214
     1176#: src/PHP/Commerce/LocalHandler.php:218
    5621177#, php-format
    5631178msgid "Order %1$s is currently: %2$s (placed on %3$s)."
    5641179msgstr ""
    5651180
    566 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:197
    5671181#: src/PHP/Commerce/Woo/LeadService.php:197
    5681182msgid "Human support needed"
    5691183msgstr ""
    5701184
    571 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:198
    5721185#: src/PHP/Commerce/Woo/LeadService.php:198
    5731186msgid "Priority:"
    5741187msgstr ""
    5751188
    576 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:199
    5771189#: src/PHP/Commerce/Woo/LeadService.php:199
    5781190msgid "Issue:"
    5791191msgstr ""
    5801192
    581 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:200
    5821193#: src/PHP/Commerce/Woo/LeadService.php:200
    5831194msgid "Summary:"
    … …  
    5851196
    5861197#. translators: %d: conversation ID.
    587 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:202
    5881198#: src/PHP/Commerce/Woo/LeadService.php:202
    5891199#, php-format
    … …  
    5911201msgstr ""
    5921202
    593 #: build/ninhle-ai-sales-assistant/src/PHP/Core/JobDispatcher.php:69
    594 #: premium/REST/TelegramController.php:55
    595 #: src/PHP/Core/JobDispatcher.php:69
    596 msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly."
    597 msgstr ""
    598 
    599 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:35
    600 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:43
     1203#: src/PHP/Core/AsyncKick.php:342
     1204msgid "Ninhle AI Sales Assistant: background replies are blocked by your host"
     1205msgstr ""
     1206
     1207#: src/PHP/Core/AsyncKick.php:343
     1208msgid "The chat still works, but each AI reply now runs while the customer's chat window waits, which keeps a server worker busy for several seconds per reply."
     1209msgstr ""
     1210
     1211#: src/PHP/Core/AsyncKick.php:344
     1212msgid "Ask your host to allow WordPress loopback requests (Tools > Site Health shows the same check) and to let PHP finish requests in the background. The plugin checks again once a day and when you save settings."
     1213msgstr ""
     1214
    6011215#: src/PHP/Privacy/PersonalData.php:35
    6021216#: src/PHP/Privacy/PersonalData.php:43
    … …  
    6041218msgstr ""
    6051219
    606 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:144
    607 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:206
    6081220#: src/PHP/Privacy/PersonalData.php:144
    609 #: src/PHP/Privacy/PersonalData.php:206
     1221#: src/PHP/Privacy/PersonalData.php:229
    6101222msgid "Conversation"
    6111223msgstr ""
    6121224
    613 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:145
    6141225#: src/PHP/Privacy/PersonalData.php:145
    6151226msgid "Sender"
    6161227msgstr ""
    6171228
    618 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:146
    6191229#: src/PHP/Privacy/PersonalData.php:146
    6201230msgid "Message"
    6211231msgstr ""
    6221232
    623 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:147
    6241233#: src/PHP/Privacy/PersonalData.php:147
    6251234msgid "Sent"
    6261235msgstr ""
    6271236
    628 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:153
    6291237#: src/PHP/Privacy/PersonalData.php:153
    6301238msgid "Sender first name"
    6311239msgstr ""
    6321240
    633 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:154
    6341241#: src/PHP/Privacy/PersonalData.php:154
    6351242msgid "Sender last name"
    6361243msgstr ""
    6371244
    638 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:155
    6391245#: src/PHP/Privacy/PersonalData.php:155
    6401246msgid "Sender avatar"
    6411247msgstr ""
    6421248
    643 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:164
    6441249#: src/PHP/Privacy/PersonalData.php:164
    6451250msgid "Ninhle AI Sales Assistant chat messages"
    6461251msgstr ""
    6471252
    648 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:203
    649 #: src/PHP/Privacy/PersonalData.php:203
     1253#: src/PHP/Privacy/PersonalData.php:205
     1254msgid "Order linked to this chat"
     1255msgstr ""
     1256
     1257#: src/PHP/Privacy/PersonalData.php:208
     1258msgid "Question the assistant could not answer (contact details hidden)"
     1259msgstr ""
     1260
     1261#: src/PHP/Privacy/PersonalData.php:211
     1262msgid "Customer identified in this chat"
     1263msgstr ""
     1264
     1265#. translators: %d: WordPress user ID of the logged-in customer.
     1266#: src/PHP/Privacy/PersonalData.php:213
     1267#, php-format
     1268msgid "Logged-in account #%d"
     1269msgstr ""
     1270
     1271#. translators: %d: WooCommerce order ID whose ownership the customer proved in the chat.
     1272#: src/PHP/Privacy/PersonalData.php:215
     1273#, php-format
     1274msgid "Order ID %d (ownership checked)"
     1275msgstr ""
     1276
     1277#: src/PHP/Privacy/PersonalData.php:218
     1278msgid "Reason the chat did not lead to an order (AI label)"
     1279msgstr ""
     1280
     1281#: src/PHP/Privacy/PersonalData.php:222
     1282msgid "Answer to \"Did this answer your question?\""
     1283msgstr ""
     1284
     1285#: src/PHP/Privacy/PersonalData.php:226
    6501286msgid "Ninhle AI Sales Assistant chat statistics"
    6511287msgstr ""
    6521288
    653 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:208
    654 #: src/PHP/Privacy/PersonalData.php:208
    655 msgid "Order linked to this chat"
    656 msgstr ""
    657 
    658 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:209
    659 #: src/PHP/Privacy/PersonalData.php:209
    660 msgid "Answer to \"Did this answer your question?\""
    661 msgstr ""
    662 
    663 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:210
    664 #: src/PHP/Privacy/PersonalData.php:210
     1289#: src/PHP/Privacy/PersonalData.php:231
    6651290msgid "Recorded (UTC)"
    6661291msgstr ""
    6671292
    668 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:228
    669 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:145
    670 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:89
    671 #: premium/CRM/CrmStore.php:610
    672 #: src/PHP/Privacy/PersonalData.php:228
    673 #: src/PHP/REST/LeadController.php:145
    674 #: src/PHP/Widget/WidgetLoader.php:89
    675 msgid "Name"
    676 msgstr ""
    677 
    678 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:229
    679 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:146
    680 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:90
    681 #: src/PHP/Privacy/PersonalData.php:229
     1293#: src/PHP/Privacy/PersonalData.php:250
    6821294#: src/PHP/REST/LeadController.php:146
    683 #: src/PHP/Widget/WidgetLoader.php:90
     1295#: src/PHP/Widget/WidgetLoader.php:95
    6841296msgid "Email"
    6851297msgstr ""
    6861298
    687 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:230
    688 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:147
    689 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:91
    690 #: src/PHP/Privacy/PersonalData.php:230
     1299#: src/PHP/Privacy/PersonalData.php:251
    6911300#: src/PHP/REST/LeadController.php:147
    692 #: src/PHP/Widget/WidgetLoader.php:91
     1301#: src/PHP/Widget/WidgetLoader.php:96
    6931302msgid "Phone"
    6941303msgstr ""
    6951304
    696 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:231
    697 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:148
    698 #: premium/CRM/CrmStore.php:626
    699 #: premium/CRM/CrmStore.php:676
    700 #: src/PHP/Privacy/PersonalData.php:231
    701 #: src/PHP/REST/LeadController.php:148
    702 msgid "Status"
    703 msgstr ""
    704 
    705 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:232
    706 #: src/PHP/Privacy/PersonalData.php:232
     1305#: src/PHP/Privacy/PersonalData.php:253
    7071306msgid "Lead score"
    7081307msgstr ""
    7091308
    710 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:233
    711 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:150
    712 #: premium/CRM/CrmStore.php:640
    713 #: premium/CRM/CrmStore.php:678
    714 #: src/PHP/Privacy/PersonalData.php:233
    715 #: src/PHP/REST/LeadController.php:150
    716 msgid "Created"
    717 msgstr ""
    718 
    719 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:240
    720 #: src/PHP/Privacy/PersonalData.php:240
     1309#: src/PHP/Privacy/PersonalData.php:261
    7211310msgid "Additional fields"
    7221311msgstr ""
    7231312
    724 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:247
    725 #: src/PHP/Privacy/PersonalData.php:247
     1313#: src/PHP/Privacy/PersonalData.php:268
    7261314msgid "Ninhle AI Sales Assistant contact details"
    7271315msgstr ""
    7281316
    729 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:272
    730 #: src/PHP/Privacy/PersonalData.php:272
     1317#: src/PHP/Privacy/PersonalData.php:293
    7311318msgid "Ninhle AI Sales Assistant chat sessions"
    7321319msgstr ""
    7331320
    734 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:275
    735 #: premium/CRM/CrmStore.php:625
    736 #: premium/CRM/CrmStore.php:666
    737 #: src/PHP/Privacy/PersonalData.php:275
    738 msgid "Channel"
    739 msgstr ""
    740 
    741 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:276
    742 #: src/PHP/Privacy/PersonalData.php:276
     1321#: src/PHP/Privacy/PersonalData.php:297
    7431322msgid "Session reference"
    7441323msgstr ""
    7451324
    746 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:277
    747 #: src/PHP/Privacy/PersonalData.php:277
     1325#: src/PHP/Privacy/PersonalData.php:298
    7481326msgid "First seen"
    7491327msgstr ""
    7501328
    751 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:310
    752 #: src/PHP/Privacy/PersonalData.php:310
     1329#: src/PHP/Privacy/PersonalData.php:331
    7531330msgid "Ninhle AI Sales Assistant activity log"
    7541331msgstr ""
    7551332
    756 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:313
    757 #: src/PHP/Privacy/PersonalData.php:313
     1333#: src/PHP/Privacy/PersonalData.php:334
    7581334msgid "Action"
    7591335msgstr ""
    7601336
    761 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:314
    762 #: src/PHP/Privacy/PersonalData.php:314
     1337#: src/PHP/Privacy/PersonalData.php:335
    7631338msgid "IP address"
    7641339msgstr ""
    7651340
    766 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:315
    767 #: src/PHP/Privacy/PersonalData.php:315
     1341#: src/PHP/Privacy/PersonalData.php:336
    7681342msgid "Browser"
    7691343msgstr ""
    7701344
    771 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:316
    772 #: premium/CRM/CrmStore.php:668
    773 #: src/PHP/Privacy/PersonalData.php:316
    774 msgid "When"
    775 msgstr ""
    776 
    777 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:409
    778 #: src/PHP/Privacy/PersonalData.php:409
     1345#: src/PHP/Privacy/PersonalData.php:430
    7791346msgid "Conversation text that was previously sent to an enabled third-party AI provider is subject to that provider's own retention policy and cannot be deleted from here."
    7801347msgstr ""
    7811348
    782 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:530
    783 #: src/PHP/Privacy/PersonalData.php:530
     1349#: src/PHP/Privacy/PersonalData.php:551
    7841350msgid "This person was linked to records in a connected CRM. Copies already synced there must be erased in that CRM."
    7851351msgstr ""
    7861352
    787 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:538
    788 #: src/PHP/Privacy/PersonalData.php:538
     1353#: src/PHP/Privacy/PersonalData.php:559
    7891354msgid "Customer records in Ninhle AI Sales Assistant CRM could not be erased because of a database error. Please run the erasure again."
    7901355msgstr ""
    7911356
    792 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:42
    7931357#: src/PHP/Privacy/PolicyContent.php:42
    7941358msgid "This site uses Ninhle AI Sales Assistant, an AI sales assistant. When you use the chat widget, the following information is stored in this site's own database: the messages you send and receive, the page you started the conversation on, and any name, email address or phone number you choose to provide."
    7951359msgstr ""
    7961360
    797 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:46
    7981361#: src/PHP/Privacy/PolicyContent.php:46
    7991362msgid "Your IP address and browser identification are recorded alongside each message, so the store can investigate abuse of the chat. This information is deleted together with your conversation."
    8001363msgstr ""
    8011364
    802 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:49
    8031365#: src/PHP/Privacy/PolicyContent.php:49
    8041366msgid "The chat widget stores two kinds of item in your browser's local storage: a session token that lets you return to your conversation, and a note that the \"Did this answer your question?\" prompt was already shown, so it is not asked twice. They are not cookies, they are not used for tracking, and they are not shared with any other site. Clearing your browser data removes them."
    8051367msgstr ""
    8061368
    807 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:53
    8081369#: src/PHP/Privacy/PolicyContent.php:53
    8091370msgid "When the assistant shows you products from the store, or you click a product or cart link in the chat, the conversation number is saved in your shopping session (the store's WooCommerce session cookie). If you then place an order within the store's attribution window, the order is linked to that conversation so the store can count orders the assistant helped with. The link is removed when your conversation data is deleted; the order itself is kept as usual."
    8101371msgstr ""
    8111372
    812 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:55
    8131373#: src/PHP/Privacy/PolicyContent.php:55
    8141374msgid "If you answer \"Did this answer your question?\" in the chat, your answer is stored with the conversation and used only for the store's own statistics."
    … …  
    8161376
    8171377#. translators: %s is the name of the configured AI provider.
    818 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:59
    8191378#: src/PHP/Privacy/PolicyContent.php:59
    8201379#, php-format
    … …  
    8221381msgstr ""
    8231382
    824 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:64
    8251383#: src/PHP/Privacy/PolicyContent.php:64
    8261384msgid "When you leave your contact details, or when a conversation is passed to a human, a notification containing those details is sent to the store team over Telegram."
    8271385msgstr ""
    8281386
    829 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:68
    8301387#: src/PHP/Privacy/PolicyContent.php:68
    8311388msgid "If you paste a web address into the chat, that address is sent to the third-party service r.jina.ai so its page content can be read."
    8321389msgstr ""
    8331390
    834 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:73
    8351391#: src/PHP/Privacy/PolicyContent.php:73
    8361392msgid "If you use the microphone button in the chat, speech recognition is done by your own web browser, which turns your speech into text in the message box. Depending on your browser, the audio may be sent to the browser maker's speech recognition service (for example Google for Chrome, Apple for Safari, or Microsoft for Edge). This site never receives or stores the audio; it only receives the text you choose to send."
    8371393msgstr ""
    8381394
    839 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:77
    8401395#: src/PHP/Privacy/PolicyContent.php:77
    8411396msgid "When you leave your contact details or request a human, an event containing those details is sent to an automation system configured by the store."
    … …  
    8431398
    8441399#. translators: %d is the number of days files are kept.
    845 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:84
    8461400#: src/PHP/Privacy/PolicyContent.php:84
    8471401#, php-format
    … …  
    8511405msgstr[1] ""
    8521406
    853 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:94
    8541407#: src/PHP/Privacy/PolicyContent.php:94
    8551408msgid "You can request a copy of this data, or ask for it to be deleted, using the contact details in this privacy policy. Conversations and contact details are not shared with anyone other than the services named above."
    8561409msgstr ""
    8571410
    858 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:99
    8591411#: src/PHP/Privacy/PolicyContent.php:99
    8601412msgid "A deletion request removes the personal data held by this site. Message text that was already sent to the services named above remains subject to those services' own retention policies and cannot be deleted from here."
    8611413msgstr ""
    8621414
    863 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:109
    8641415#: src/PHP/Privacy/PolicyContent.php:109
    8651416msgid "OpenAI (see openai.com/policies/privacy-policy)"
    8661417msgstr ""
    8671418
    868 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:110
    8691419#: src/PHP/Privacy/PolicyContent.php:110
    8701420msgid "Anthropic (see anthropic.com/legal/privacy)"
    8711421msgstr ""
    8721422
    873 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:111
    8741423#: src/PHP/Privacy/PolicyContent.php:111
    8751424msgid "Google (see policies.google.com/privacy)"
    8761425msgstr ""
    8771426
    878 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:114
    8791427#: src/PHP/Privacy/PolicyContent.php:114
    8801428msgid "the AI provider configured by the store owner"
    8811429msgstr ""
    8821430
    883 #: build/ninhle-ai-sales-assistant/src/PHP/REST/AdminController.php:253
    884 #: src/PHP/REST/AdminController.php:253
     1431#: src/PHP/REST/AdminController.php:293
    8851432msgid "The Telegram Chat ID is your bot's own ID. Enter the ID of the group or person that should receive notifications, then add the bot to that group (or send the bot a message first)."
    8861433msgstr ""
    8871434
    888 #: build/ninhle-ai-sales-assistant/src/PHP/REST/ChatController.php:24
    889 #: src/PHP/REST/ChatController.php:24
     1435#: src/PHP/REST/ChatController.php:27
    8901436msgid "Sales Assistant"
    8911437msgstr ""
    8921438
    893 #: build/ninhle-ai-sales-assistant/src/PHP/REST/ChatController.php:238
    894 #: src/PHP/REST/ChatController.php:238
     1439#: src/PHP/REST/ChatController.php:289
    8951440msgid "Thanks, our team has been notified. A member of staff will reply here as soon as possible."
    8961441msgstr ""
    8971442
    898 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:98
    899 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:142
    9001443#: src/PHP/REST/Guard.php:98
    9011444#: src/PHP/REST/Guard.php:142
    … …  
    9031446msgstr ""
    9041447
    905 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:124
    9061448#: src/PHP/REST/Guard.php:124
    9071449msgid "Security check failed. Please reload the page and try again."
    9081450msgstr ""
    9091451
    910 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:165
    9111452#: src/PHP/REST/Guard.php:165
    9121453msgid "Missing chat session."
    9131454msgstr ""
    9141455
    915 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:175
    9161456#: src/PHP/REST/Guard.php:175
    9171457msgid "Invalid or expired chat session."
    9181458msgstr ""
    9191459
    920 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:184
    921 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:199
    9221460#: src/PHP/REST/Guard.php:184
    9231461#: src/PHP/REST/Guard.php:199
    … …  
    9251463msgstr ""
    9261464
    927 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:215
    9281465#: src/PHP/REST/Guard.php:215
    9291466msgid "Too many requests. Please wait a moment."
    9301467msgstr ""
    9311468
    932 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:242
    9331469#: src/PHP/REST/Guard.php:242
    9341470msgid "Telegram webhook secret is not configured."
    9351471msgstr ""
    9361472
    937 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:255
    9381473#: src/PHP/REST/Guard.php:255
    9391474msgid "Invalid webhook signature."
    9401475msgstr ""
    9411476
    942 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:308
    9431477#: src/PHP/REST/Guard.php:308
    9441478msgid "Message must be text."
    9451479msgstr ""
    9461480
    947 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:313
    9481481#: src/PHP/REST/Guard.php:313
    9491482msgid "Message contains invalid characters."
    9501483msgstr ""
    9511484
    952 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:317
    9531485#: src/PHP/REST/Guard.php:317
    9541486msgid "Message cannot be empty."
    9551487msgstr ""
    9561488
    957 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:321
    9581489#: src/PHP/REST/Guard.php:321
    9591490msgid "Message is too large."
    9601491msgstr ""
    9611492
    962 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:325
    9631493#: src/PHP/REST/Guard.php:325
    9641494msgid "Message is too long."
    9651495msgstr ""
    9661496
    967 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:149
     1497#: src/PHP/REST/InsightsController.php:43
     1498msgid "Enter both the question and the answer."
     1499msgstr ""
     1500
     1501#: src/PHP/REST/InsightsController.php:66
     1502msgid "AI insights need the Starter plan."
     1503msgstr ""
     1504
    9681505#: src/PHP/REST/LeadController.php:149
    9691506msgid "Score"
    9701507msgstr ""
    9711508
    972 #: build/ninhle-ai-sales-assistant/src/PHP/Rules/RuleEngine.php:232
    9731509#: src/PHP/Rules/RuleEngine.php:232
    9741510msgid "A member of our team will join this chat shortly."
    9751511msgstr ""
    9761512
    977 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:68
    978 #: src/PHP/Widget/WidgetLoader.php:68
     1513#: src/PHP/Widget/WidgetLoader.php:71
    9791514msgid "Open chat"
    9801515msgstr ""
    9811516
    982 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:69
    983 #: src/PHP/Widget/WidgetLoader.php:69
     1517#. translators: %d: number of replies the shopper has not read yet.
     1518#: src/PHP/Widget/WidgetLoader.php:73
     1519#, php-format
     1520msgid "new replies: %d"
     1521msgstr ""
     1522
     1523#: src/PHP/Widget/WidgetLoader.php:74
    9841524msgid "Chat with our assistant"
    9851525msgstr ""
    9861526
    987 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:70
    988 #: src/PHP/Widget/WidgetLoader.php:70
     1527#: src/PHP/Widget/WidgetLoader.php:75
    9891528msgid "Cart"
    9901529msgstr ""
    9911530
    992 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:71
    993 #: src/PHP/Widget/WidgetLoader.php:71
     1531#: src/PHP/Widget/WidgetLoader.php:76
    9941532msgid "My details"
    9951533msgstr ""
    9961534
    997 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:72
    998 #: src/PHP/Widget/WidgetLoader.php:72
     1535#: src/PHP/Widget/WidgetLoader.php:77
    9991536msgid "Clear conversation"
    10001537msgstr ""
    10011538
    1002 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:73
    1003 #: src/PHP/Widget/WidgetLoader.php:73
     1539#: src/PHP/Widget/WidgetLoader.php:78
     1540#: src/PHP/Widget/WidgetLoader.php:139
    10041541msgid "Close"
    10051542msgstr ""
    10061543
    1007 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:74
    1008 #: src/PHP/Widget/WidgetLoader.php:74
     1544#: src/PHP/Widget/WidgetLoader.php:79
    10091545msgid "Clear the conversation history and start over?"
    10101546msgstr ""
    10111547
    1012 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:75
    1013 #: src/PHP/Widget/WidgetLoader.php:75
     1548#: src/PHP/Widget/WidgetLoader.php:80
    10141549msgid "Clear"
    10151550msgstr ""
    10161551
    1017 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:76
    1018 #: src/PHP/Widget/WidgetLoader.php:76
    1019 msgid "Cancel"
    1020 msgstr ""
    1021 
    1022 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:77
    1023 #: src/PHP/Widget/WidgetLoader.php:77
     1552#: src/PHP/Widget/WidgetLoader.php:82
    10241553msgid "Attach a photo or PDF"
    10251554msgstr ""
    10261555
    1027 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:78
    1028 #: src/PHP/Widget/WidgetLoader.php:78
     1556#: src/PHP/Widget/WidgetLoader.php:83
    10291557msgid "Uploading…"
    10301558msgstr ""
    10311559
    1032 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:79
    1033 #: src/PHP/Widget/WidgetLoader.php:79
     1560#: src/PHP/Widget/WidgetLoader.php:84
    10341561msgid "File upload failed."
    10351562msgstr ""
    10361563
    1037 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:80
    1038 #: src/PHP/Widget/WidgetLoader.php:80
     1564#: src/PHP/Widget/WidgetLoader.php:85
    10391565msgid "Your message could not be sent. Please try again."
    10401566msgstr ""
    10411567
    1042 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:81
    1043 #: src/PHP/Widget/WidgetLoader.php:81
     1568#: src/PHP/Widget/WidgetLoader.php:86
    10441569msgid "Call us"
    10451570msgstr ""
    10461571
    1047 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:82
    1048 #: src/PHP/Widget/WidgetLoader.php:82
     1572#: src/PHP/Widget/WidgetLoader.php:87
    10491573msgid "Send"
    10501574msgstr ""
    10511575
    1052 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:83
    1053 #: src/PHP/Widget/WidgetLoader.php:83
     1576#: src/PHP/Widget/WidgetLoader.php:88
    10541577msgid "Your message"
    10551578msgstr ""
    10561579
    1057 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:84
    1058 #: src/PHP/Widget/WidgetLoader.php:84
     1580#: src/PHP/Widget/WidgetLoader.php:89
    10591581msgid "Type your question…"
    10601582msgstr ""
    10611583
    10621584#. translators: %s is the maximum upload size in megabytes.
    1063 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:86
    1064 #: src/PHP/Widget/WidgetLoader.php:86
     1585#: src/PHP/Widget/WidgetLoader.php:91
    10651586#, php-format
    10661587msgid "File is too large. Please choose a file under %sMB."
    10671588msgstr ""
    10681589
    1069 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:87
    1070 #: src/PHP/Widget/WidgetLoader.php:87
     1590#: src/PHP/Widget/WidgetLoader.php:92
    10711591msgid "Thank you. Our team will contact you shortly."
    10721592msgstr ""
    10731593
    1074 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:88
    1075 #: src/PHP/Widget/WidgetLoader.php:88
     1594#: src/PHP/Widget/WidgetLoader.php:93
    10761595msgid "Your contact details"
    10771596msgstr ""
    10781597
    1079 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:92
    1080 #: src/PHP/Widget/WidgetLoader.php:92
     1598#: src/PHP/Widget/WidgetLoader.php:97
    10811599msgid "Submit"
    10821600msgstr ""
    10831601
    1084 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:93
    1085 #: src/PHP/Widget/WidgetLoader.php:93
     1602#: src/PHP/Widget/WidgetLoader.php:98
    10861603msgid "Submitting…"
    10871604msgstr ""
    10881605
    1089 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:94
    1090 #: src/PHP/Widget/WidgetLoader.php:94
     1606#: src/PHP/Widget/WidgetLoader.php:99
    10911607msgid "Please provide at least an email or phone number."
    10921608msgstr ""
    10931609
    1094 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:95
    1095 #: src/PHP/Widget/WidgetLoader.php:95
     1610#: src/PHP/Widget/WidgetLoader.php:100
    10961611msgid "Could not submit your details."
    10971612msgstr ""
    10981613
    1099 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:97
    1100 #: src/PHP/Widget/WidgetLoader.php:97
     1614#: src/PHP/Widget/WidgetLoader.php:102
    11011615msgid "More options"
    11021616msgstr ""
    11031617
    1104 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:98
    1105 #: src/PHP/Widget/WidgetLoader.php:98
     1618#: src/PHP/Widget/WidgetLoader.php:103
    11061619msgid "Expand"
    11071620msgstr ""
    11081621
    1109 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:99
    1110 #: src/PHP/Widget/WidgetLoader.php:99
     1622#: src/PHP/Widget/WidgetLoader.php:104
    11111623msgid "Make smaller"
    11121624msgstr ""
    11131625
    1114 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:100
    1115 #: src/PHP/Widget/WidgetLoader.php:100
     1626#: src/PHP/Widget/WidgetLoader.php:105
    11161627msgid "Talk to a person"
    11171628msgstr ""
    11181629
    1119 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:101
    1120 #: src/PHP/Widget/WidgetLoader.php:101
     1630#: src/PHP/Widget/WidgetLoader.php:106
    11211631msgid "AI assistant"
    11221632msgstr ""
    11231633
    1124 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:102
    1125 #: src/PHP/Widget/WidgetLoader.php:102
     1634#: src/PHP/Widget/WidgetLoader.php:107
    11261635msgid "Waiting for a team member"
    11271636msgstr ""
    11281637
    11291638#. translators: %s is the first name of the staff member answering the chat.
    1130 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:104
    1131 #: src/PHP/Widget/WidgetLoader.php:104
     1639#: src/PHP/Widget/WidgetLoader.php:109
    11321640#, php-format
    11331641msgid "%s is helping you"
    11341642msgstr ""
    11351643
    1136 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:105
    1137 #: src/PHP/Widget/WidgetLoader.php:105
     1644#: src/PHP/Widget/WidgetLoader.php:110
    11381645msgid "A team member is helping you"
    11391646msgstr ""
    11401647
    1141 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:106
    1142 #: src/PHP/Widget/WidgetLoader.php:106
     1648#: src/PHP/Widget/WidgetLoader.php:111
    11431649msgid "This conversation is closed"
    11441650msgstr ""
    11451651
    1146 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:107
    1147 #: src/PHP/Widget/WidgetLoader.php:107
     1652#: src/PHP/Widget/WidgetLoader.php:112
    11481653msgid "Writing a reply…"
    11491654msgstr ""
    11501655
    1151 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:108
    1152 #: src/PHP/Widget/WidgetLoader.php:108
     1656#: src/PHP/Widget/WidgetLoader.php:113
    11531657msgid "Checking product information…"
    11541658msgstr ""
    11551659
    1156 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:109
    1157 #: src/PHP/Widget/WidgetLoader.php:109
     1660#: src/PHP/Widget/WidgetLoader.php:114
    11581661msgid "Looking up your order…"
    11591662msgstr ""
    11601663
    1161 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:110
    1162 #: src/PHP/Widget/WidgetLoader.php:110
     1664#: src/PHP/Widget/WidgetLoader.php:115
    11631665msgid "Connecting you with a team member…"
    11641666msgstr ""
    11651667
    1166 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:111
    1167 #: src/PHP/Widget/WidgetLoader.php:111
     1668#: src/PHP/Widget/WidgetLoader.php:116
     1669msgid "Waiting in line…"
     1670msgstr ""
     1671
     1672#: src/PHP/Widget/WidgetLoader.php:119
     1673msgid "Previous products"
     1674msgstr ""
     1675
     1676#: src/PHP/Widget/WidgetLoader.php:120
     1677msgid "Next products"
     1678msgstr ""
     1679
     1680#: src/PHP/Widget/WidgetLoader.php:121
     1681msgid "View"
     1682msgstr ""
     1683
     1684#: src/PHP/Widget/WidgetLoader.php:122
     1685msgid "In stock"
     1686msgstr ""
     1687
     1688#: src/PHP/Widget/WidgetLoader.php:124
     1689msgid "On backorder"
     1690msgstr ""
     1691
     1692#. translators: %s: date the price was shown, e.g. "24 Oct 2026".
     1693#: src/PHP/Widget/WidgetLoader.php:126
     1694#, php-format
     1695msgid "Price on %s"
     1696msgstr ""
     1697
     1698#: src/PHP/Widget/WidgetLoader.php:128
     1699msgid "Add to cart"
     1700msgstr ""
     1701
     1702#: src/PHP/Widget/WidgetLoader.php:129
     1703msgid "Adding…"
     1704msgstr ""
     1705
     1706#: src/PHP/Widget/WidgetLoader.php:130
     1707msgid "Choose options"
     1708msgstr ""
     1709
     1710#: src/PHP/Widget/WidgetLoader.php:131
     1711msgid "Added to cart"
     1712msgstr ""
     1713
     1714#: src/PHP/Widget/WidgetLoader.php:132
     1715msgid "View cart"
     1716msgstr ""
     1717
     1718#: src/PHP/Widget/WidgetLoader.php:133
     1719msgid "Checkout"
     1720msgstr ""
     1721
     1722#: src/PHP/Widget/WidgetLoader.php:134
     1723msgid "Sorry, this item just sold out."
     1724msgstr ""
     1725
     1726#: src/PHP/Widget/WidgetLoader.php:135
     1727msgid "You can't add more of this item."
     1728msgstr ""
     1729
     1730#: src/PHP/Widget/WidgetLoader.php:136
     1731msgid "Couldn't add to cart."
     1732msgstr ""
     1733
     1734#: src/PHP/Widget/WidgetLoader.php:137
     1735msgid "Open the product page"
     1736msgstr ""
     1737
     1738#: src/PHP/Widget/WidgetLoader.php:138
     1739msgid "Your cart"
     1740msgstr ""
     1741
     1742#: src/PHP/Widget/WidgetLoader.php:140
     1743msgid "Loading your cart…"
     1744msgstr ""
     1745
     1746#: src/PHP/Widget/WidgetLoader.php:141
     1747msgid "Couldn't load your cart."
     1748msgstr ""
     1749
     1750#: src/PHP/Widget/WidgetLoader.php:143
     1751msgid "Subtotal"
     1752msgstr ""
     1753
     1754#: src/PHP/Widget/WidgetLoader.php:144
     1755msgid "Discount"
     1756msgstr ""
     1757
     1758#: src/PHP/Widget/WidgetLoader.php:146
     1759msgid "Your orders"
     1760msgstr ""
     1761
     1762#. translators: %s: order number.
     1763#: src/PHP/Widget/WidgetLoader.php:148
     1764#, php-format
     1765msgid "Order #%s"
     1766msgstr ""
     1767
     1768#. translators: %d: number of other items in the order.
     1769#: src/PHP/Widget/WidgetLoader.php:150
     1770#, php-format
     1771msgid "and %d more"
     1772msgstr ""
     1773
     1774#: src/PHP/Widget/WidgetLoader.php:151
     1775msgid "Tracking number"
     1776msgstr ""
     1777
     1778#: src/PHP/Widget/WidgetLoader.php:152
     1779msgid "Track"
     1780msgstr ""
     1781
     1782#: src/PHP/Widget/WidgetLoader.php:153
     1783msgid "View order"
     1784msgstr ""
     1785
     1786#: src/PHP/Widget/WidgetLoader.php:154
    11681787msgid "Did this answer your question?"
    11691788msgstr ""
    11701789
    1171 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:112
    1172 #: premium/CRM/CrmStore.php:611
    1173 #: src/PHP/Widget/WidgetLoader.php:112
    1174 msgid "Yes"
    1175 msgstr ""
    1176 
    1177 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:113
    1178 #: src/PHP/Widget/WidgetLoader.php:113
     1790#: src/PHP/Widget/WidgetLoader.php:156
    11791791msgid "Not yet"
    11801792msgstr ""
    11811793
    1182 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:114
    1183 #: src/PHP/Widget/WidgetLoader.php:114
     1794#: src/PHP/Widget/WidgetLoader.php:157
    11841795msgid "Skip"
    11851796msgstr ""
    11861797
    1187 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:115
    1188 #: src/PHP/Widget/WidgetLoader.php:115
     1798#: src/PHP/Widget/WidgetLoader.php:158
    11891799msgid "Thanks for letting us know."
    11901800msgstr ""
    11911801
    1192 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:116
    1193 #: src/PHP/Widget/WidgetLoader.php:116
     1802#: src/PHP/Widget/WidgetLoader.php:159
    11941803msgid "Other ways to reach us"
    11951804msgstr ""
    11961805
    1197 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:118
    1198 #: src/PHP/Widget/WidgetLoader.php:118
     1806#: src/PHP/Widget/WidgetLoader.php:161
    11991807msgid "Speak your message"
    12001808msgstr ""
    12011809
    1202 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:119
    1203 #: src/PHP/Widget/WidgetLoader.php:119
     1810#: src/PHP/Widget/WidgetLoader.php:162
    12041811msgid "Stop listening"
    12051812msgstr ""
    12061813
    1207 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:120
    1208 #: src/PHP/Widget/WidgetLoader.php:120
     1814#: src/PHP/Widget/WidgetLoader.php:163
    12091815msgid "Listening…"
    12101816msgstr ""
    12111817
    1212 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:121
    1213 #: src/PHP/Widget/WidgetLoader.php:121
     1818#: src/PHP/Widget/WidgetLoader.php:164
    12141819msgid "Microphone access is blocked. Allow it in your browser settings, or type your message."
    12151820msgstr ""
    12161821
    1217 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:122
    1218 #: src/PHP/Widget/WidgetLoader.php:122
     1822#: src/PHP/Widget/WidgetLoader.php:165
    12191823msgid "No speech was heard. Please try again."
    12201824msgstr ""
    12211825
    1222 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:123
    1223 #: src/PHP/Widget/WidgetLoader.php:123
     1826#: src/PHP/Widget/WidgetLoader.php:166
    12241827msgid "No microphone was found. Please type your message."
    12251828msgstr ""
    12261829
    1227 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:124
    1228 #: src/PHP/Widget/WidgetLoader.php:124
     1830#: src/PHP/Widget/WidgetLoader.php:167
    12291831msgid "Voice input needs an internet connection. Please type your message."
    12301832msgstr ""
    12311833
    1232 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:125
    1233 #: src/PHP/Widget/WidgetLoader.php:125
     1834#: src/PHP/Widget/WidgetLoader.php:168
    12341835msgid "Your browser cannot recognise this language. Please type your message."
    12351836msgstr ""
    12361837
    1237 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:126
    1238 #: src/PHP/Widget/WidgetLoader.php:126
     1838#: src/PHP/Widget/WidgetLoader.php:169
    12391839msgid "Voice input is not available right now. Please type your message."
    12401840msgstr ""
    12411841
    1242 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:164
    1243 #: src/PHP/Widget/WidgetLoader.php:164
     1842#: src/PHP/Widget/WidgetLoader.php:215
    12441843msgid "You are chatting with an AI assistant. A team member can join when needed."
    12451844msgstr ""
    1246 
    1247 #: premium/CRM/CrmStore.php:609
    1248 msgid "Customer ID"
    1249 msgstr ""
    1250 
    1251 #: premium/CRM/CrmStore.php:611
    1252 msgid "Verified"
    1253 msgstr ""
    1254 
    1255 #: premium/CRM/CrmStore.php:611
    1256 msgid "No"
    1257 msgstr ""
    1258 
    1259 #. translators: %s: contact type, for example "email" or "phone".
    1260 #: premium/CRM/CrmStore.php:616
    1261 #, php-format
    1262 msgid "Contact (%s)"
    1263 msgstr ""
    1264 
    1265 #: premium/CRM/CrmStore.php:620
    1266 msgid "Ninhle AI Sales Assistant customer profile"
    1267 msgstr ""
    1268 
    1269 #: premium/CRM/CrmStore.php:623
    1270 msgid "Ninhle AI Sales Assistant contact permissions"
    1271 msgstr ""
    1272 
    1273 #: premium/CRM/CrmStore.php:624
    1274 msgid "Purpose"
    1275 msgstr ""
    1276 
    1277 #: premium/CRM/CrmStore.php:627
    1278 msgid "Source"
    1279 msgstr ""
    1280 
    1281 #: premium/CRM/CrmStore.php:628
    1282 msgid "Recorded"
    1283 msgstr ""
    1284 
    1285 #: premium/CRM/CrmStore.php:634
    1286 msgid "Ninhle AI Sales Assistant purchase opportunities"
    1287 msgstr ""
    1288 
    1289 #: premium/CRM/CrmStore.php:635
    1290 msgid "Need"
    1291 msgstr ""
    1292 
    1293 #: premium/CRM/CrmStore.php:636
    1294 msgid "Stage"
    1295 msgstr ""
    1296 
    1297 #: premium/CRM/CrmStore.php:637
    1298 msgid "Payment"
    1299 msgstr ""
    1300 
    1301 #: premium/CRM/CrmStore.php:638
    1302 msgid "Products"
    1303 msgstr ""
    1304 
    1305 #: premium/CRM/CrmStore.php:664
    1306 msgid "Ninhle AI Sales Assistant customer history"
    1307 msgstr ""
    1308 
    1309 #: premium/CRM/CrmStore.php:665
    1310 #: premium/CRM/CrmStore.php:675
    1311 msgid "Type"
    1312 msgstr ""
    1313 
    1314 #: premium/CRM/CrmStore.php:667
    1315 msgid "Content"
    1316 msgstr ""
    1317 
    1318 #: premium/CRM/CrmStore.php:674
    1319 msgid "Ninhle AI Sales Assistant support and follow-up requests"
    1320 msgstr ""
    1321 
    1322 #: premium/CRM/CrmStore.php:677
    1323 msgid "Resolution"
    1324 msgstr ""
    1325 
    1326 #: premium/Integrations/TelegramService.php:77
    1327 msgid "None"
    1328 msgstr ""
    1329 
    1330 #. translators: %s: chat widget brand name.
    1331 #: premium/Integrations/TelegramService.php:79
    1332 #, php-format
    1333 msgid "New lead from %s"
    1334 msgstr ""
    1335 
    1336 #: premium/Integrations/TelegramService.php:80
    1337 msgid "Name:"
    1338 msgstr ""
    1339 
    1340 #: premium/Integrations/TelegramService.php:81
    1341 msgid "Email:"
    1342 msgstr ""
    1343 
    1344 #: premium/Integrations/TelegramService.php:82
    1345 msgid "Phone:"
    1346 msgstr ""
    1347 
    1348 #: premium/Webhooks/WebhookDispatcher.php:113
    1349 msgid "Outgoing webhooks are available in the Pro plan."
    1350 msgstr ""
  • ninhle-ai-sales-assistant/trunk/ninhle-ai-sales-assistant.php

    r3761175 r3765552  
    11<?php
    22/**
    3  * Plugin Name: Ninhle AI Sales Assistant for WooCommerce
    4  * Description: AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff.
    5  * Version:     2.5.0
     3 * Plugin Name: Ninhle AI Sales Assistant – AI Chatbot for WooCommerce
     4 * Description: AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in the chat, tracks orders and shows why shoppers don't buy.
     5 * Version:     2.6.0
    66 * Author:      Ninhle
    77 * Author URI:  https://ninhle.com
    … …  
    3737
    3838// Define Plugin Constants
    39 define('SMBCOMMERCE_VERSION', '2.5.0');
     39define('SMBCOMMERCE_VERSION', '2.6.0');
    4040define('SMBCOMMERCE_FILE', __FILE__);
    4141define('SMBCOMMERCE_PATH', plugin_dir_path(__FILE__));
  • ninhle-ai-sales-assistant/trunk/package-lock.json

    r3761175 r3765552  
    11{
    22  "name": "ninhle-ai-sales-assistant-frontend",
    3   "version": "2.5.0",
     3  "version": "2.6.0",
    44  "lockfileVersion": 3,
    55  "requires": true,
    … …  
    77    "": {
    88      "name": "ninhle-ai-sales-assistant-frontend",
    9       "version": "2.5.0",
     9      "version": "2.6.0",
    1010      "dependencies": {
    1111        "preact": "^10.23.1",
  • ninhle-ai-sales-assistant/trunk/package.json

    r3761175 r3765552  
    22  "name": "ninhle-ai-sales-assistant-frontend",
    33  "private": true,
    4   "version": "2.5.0",
     4  "version": "2.6.0",
    55  "type": "module",
    66  "scripts": {
  • ninhle-ai-sales-assistant/trunk/readme.txt

    r3762743 r3765552  
    55Tested up to: 7.1
    66Requires PHP: 8.1
    7 Stable tag: 2.5.0
     7Stable tag: 2.6.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 AI chatbot and shopping assistant for WooCommerce: answers from your real catalog, tracks orders, hands off to your team, shows AI-assisted orders.
     11AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in chat, tracks orders and shows why shoppers don't buy.
    1212
    1313== Description ==
    1414
    15 **Turn shopper questions into orders.** Ninhle AI Sales Assistant is an AI chatbot for WooCommerce: a shopping assistant for your customers and a sales assistant for you. It answers in seconds from the prices, stock and product details already in your store, looks up orders, brings in your team when needed, and shows you which orders followed a chat.
     15**Turn shopper questions into orders.** Ninhle AI Sales Assistant is an AI chatbot and shopping assistant for WooCommerce. It answers from your real catalog, adds products to the cart right in the chat, tracks orders, and shows you why shoppers ask but don't buy.
    1616
    1717Shoppers ask the same things all day: *Is it in stock? Does it come in M? How long is shipping? Where is my order?* When nobody answers, many of them leave. Unlike a generic ecommerce chatbot that only knows what it was trained on, this WooCommerce chatbot answers from live lookups against your store, and says so when it does not know.
    … …  
    1919= Get started in three steps =
    2020
    21 1. **Connect an AI provider.** Paste your own API key from OpenAI, Anthropic (Claude) or Google (Gemini). You pay the provider directly. Or start in local-only mode with no key at all.
     211. **Connect an AI provider.** Paste your own API key from OpenAI, Anthropic (Claude) or Google (Gemini), or use the AI connection built into WordPress 7.0 and later. You pay the provider directly. Or start in local-only mode with no key at all.
    22222. **Sync your catalog.** One click, or `wp ninhle-ai-sales-assistant sync-products`, and the assistant knows your products, prices and stock.
    23233. **Go live.** The chat appears on your store with your name, logo, colour and suggested questions.
    2424
    25 = An AI chatbot that answers from your real catalog =
    26 
    27 Product facts come from your WooCommerce store: prices, stock, product details, store policies and your own pages. If the catalog has not been synced, the assistant says so and offers a person instead of guessing. It never invents prices or stock.
    28 
    29 = A shopping assistant that sells, not just chats =
    30 
    31 An editable sales playbook sets the tone, the qualifying questions, how to handle objections and how to close. The assistant recommends products from your catalog and links shoppers straight to them, so a question turns into a product page, a cart and a checkout.
     25= Add to cart in the chat =
     26
     27Product answers come with product cards: photo, current price, stock and rating, read from WooCommerce when the reply is shown. Shoppers tap **Add to cart** without leaving the chat. WooCommerce updates the cart in the shopper's own browser, so your prices, stock rules and discounts apply as usual, and a cart view in the chat leads to Checkout.
     28
     29In the free version, products with options link to the product page. The Starter plan lets shoppers pick options, change quantities and apply the discount codes you allow, right on the card.
     30
     31= Answers from your real catalog =
     32
     33Product facts come from your WooCommerce store: prices, stock, product details, store policies and your own pages. When a search finds nothing, the assistant suggests the nearest categories and asks one question instead of guessing. It never invents prices or stock, and products you hide from the catalog are never shown.
    3234
    3335= Order tracking in the chat =
    3436
    35 Customers ask "Where is my order?" and get the status right in the chat once they give the order number with the email or phone number used for it. Nobody on your team has to look it up.
     37"Where is my order?" gets an order card in the chat: status, items, total and tracking number with a Track link (from WooCommerce Shipment Tracking, Advanced Shipment Tracking or an order field you choose). Logged-in customers see their recent orders straight away; guests give the order number with the email or phone number used for it. Without that proof, nothing is revealed.
     38
     39= Replies in the shopper's language =
     40
     41The assistant detects the language of each message (15 languages, without an AI call) and answers in it, keeping product names, prices and order numbers exactly as in your store. You can keep every reply in your store's language instead.
     42
     43= Sales Insights: why shoppers don't buy =
     44
     45The Sales Insights tab lists the questions the assistant could not answer, grouped and ranked; type your answer there and the assistant uses it from the next reply. It also shows what shoppers searched for but could not find, and which products they wanted while out of stock. Contact details are hidden.
     46
     47In the Starter plan, an AI labels each night why finished chats did not lead to an order (price, shipping, size, stock, missing information and more), with quotes, and a weekly summary can be emailed to you.
    3648
    3749= A person is one tap away =
    3850
    39 "Talk to a person" is always visible, and the assistant hands off complaints, explicit requests and answers it is not sure about. Your team replies from the Inbox in WP Admin, and a webhook you choose can be notified.
     51"Talk to a person" is always visible, and the assistant hands off complaints, explicit requests and questions your store data cannot answer. Your team replies from the Inbox in WP Admin, and a badge on the minimized chat button shows new replies. In the Starter plan, the Inbox shows a customer card for logged-in customers and those who proved an order: orders, total spent, last order and recent products.
    4052
    4153= See the orders the chatbot helped close =
    … …  
    4355The AI Value & Performance tab shows what the AI cost, how many orders followed a chat and how conversations ended, each with its sample size, from the day you install. Leads left in the chat stay in your own database and export to CSV.
    4456
     57= Ready for AI agents (MCP) =
     58
     59On WordPress 6.9 and later, turn on "AI agents" in Settings to register read-only abilities with the WordPress Abilities API: search products, product details and store policies. The MCP Adapter plugin can offer them to MCP clients. Only published, visible products and your store policies are shared, never orders, carts or customer data. Off by default, limited to 60 requests per hour per IP address.
     60
    4561= Your key, your data, no per-conversation fee =
    4662
    47 Use your own OpenAI, Anthropic Claude or Google Gemini API key and pay the provider directly. Requests go from your server to that provider; there is no middleman server and no per-chat fee from us. Simple greetings and store policy, cart and order-status questions are answered from store data without an AI call whenever the store has the answer, which keeps the cost down.
     63Use your own OpenAI, Anthropic Claude or Google Gemini API key and pay the provider directly, or use the AI provider connected in WordPress 7.0+ (Settings > Connectors). There is no middleman server and no per-chat fee from us. Greetings, store policies, cart questions and order status are answered from store data without an AI call when the store has the answer, and AI replies run in the background so a slow provider does not hold up your pages.
    4864
    4965= Everything in the free WooCommerce chatbot =
    5066
    51 * **Bring your own key:** Anthropic Claude, OpenAI or Google Gemini. You control the account and the spend.
    52 * **Store tools:** product search and details, order status (only after the customer proves they own the order), digital downloads, store policies and site content.
    53 * **Sales playbook and rules:** persona, qualifying questions, objection handling and closing, plus keyword rules for instant answers.
    54 * **Lead capture:** a contact form in the chat, with leads stored in your database and exportable to CSV.
    55 * **Human handoff:** visitors always see whether they are talking to the AI assistant, waiting for your team or talking to a team member. A webhook URL you choose can be notified.
    56 * **AI Value & Performance:** AI cost recorded per request from your provider's usage data and a dated price table; orders that followed a chat; conversations resolved (confirmed by the customer), handed off or unresolved.
    57 * **Conversation inbox:** read every conversation, take over from the bot and hand the chat back when you are done.
    58 * **Formatted answers:** bold text, lists, tables, links and product images display cleanly in the chat.
    59 * **Voice input (optional, off by default):** visitors can speak their message; the browser turns it into text they check before sending.
    60 * **Your brand:** widget name, logo, colour, font and suggested question chips, with a live preview.
    61 * **Local-only mode:** answer product, cart and order questions from store data without calling any AI provider.
    62 * **Light on your pages:** the chat window loads only when a visitor opens it.
    63 * **Privacy built in:** WordPress personal-data export and erasure, attachment retention, rate limiting.
    64 * **Translation-ready:** every string can be translated on translate.wordpress.org.
    65 * **WooCommerce compatible:** supports High-Performance Order Storage (HPOS) and the Cart and Checkout blocks.
     67* **Bring your own key:** Anthropic Claude, OpenAI or Google Gemini, or the AI connection in WordPress 7.0+.
     68* **Product cards and add to cart in the chat,** with a cart view.
     69* **Order tracking:** order cards with tracking numbers; "my orders" for logged-in customers.
     70* **Replies in the shopper's language** or always in your store's language.
     71* **Sales Insights:** unanswered questions, searches with no result and out-of-stock demand.
     72* **Store tools:** product search and details, order status for the order's owner, digital downloads, store policies and site content.
     73* **Sales playbook and rules:** persona, qualifying questions, objection handling, closing and keyword rules.
     74* **Lead capture** in the chat, stored in your database and exportable to CSV.
     75* **Human handoff and Inbox:** a clear status for visitors, a new-replies badge, take over and hand back, an optional webhook.
     76* **AI Value & Performance:** AI cost per request, AI-assisted orders and conversation outcomes.
     77* **Formatted answers:** lists, tables, links and product images.
     78* **Voice input** (optional, off by default).
     79* **Your brand:** name, logo, colour, font and suggested questions, with a live preview.
     80* **Local-only mode:** answers from store data without any AI provider.
     81* **Ready for AI agents** (WordPress 6.9+, off by default).
     82* **Light, private and translation-ready:** the chat loads only when needed; personal-data export and erasure; HPOS and Cart and Checkout blocks supported.
    6683
    6784= Optional paid plans =
    … …  
    7087plugin; nothing in the free version is locked or time-limited.
    7188
    72 **Starter** — product variations with per-variation add-to-cart links, Telegram
    73 notifications for new leads and handoffs, a sales dashboard, cached answers
    74 for repeated questions, and the revenue of orders that followed a chat, per currency,
    75 on the AI Value & Performance tab.
     89**Starter** — options, quantities and allowed discount codes on product cards in the chat;
     90nightly AI labels of why shoppers did not buy, with a weekly email summary; a customer card in
     91the Inbox; product variations with per-variation add-to-cart links; Telegram notifications for
     92new leads and handoffs; a sales dashboard; cached answers for repeated questions; and the
     93revenue of orders that followed a chat, per currency, on the AI Value & Performance tab.
    7694
    7795**Pro** — everything in Starter, plus coupon validation, automatic failover to a
    … …  
    91109conversation history, your sales playbook and business information, and product data from
    92110your own store, each time a visitor sends a message. Nothing is sent in local-only mode.
     111If you choose "WordPress AI connection" (WordPress 7.0+), the same data goes through WordPress
     112to the provider you connected in Settings > Connectors, under that provider's terms.
    93113
    94114* Anthropic (Claude) — https://www.anthropic.com/legal/privacy — https://www.anthropic.com/legal/consumer-terms
    95115* OpenAI — https://openai.com/policies/privacy-policy — https://openai.com/policies/terms-of-use
    96116* Google (Gemini) — https://policies.google.com/privacy — https://ai.google.dev/gemini-api/terms
     117
     118**Sales Insights AI labeling (Starter plan only, optional, off by default)** — the free
     119version contains no code for this. If you turn it on, once a night the text of finished chats
     120that did not lead to an order is sent to your AI provider, with email addresses and phone
     121numbers replaced by placeholders, so the AI can label why the shopper did not buy. A monthly
     122spending limit you set stops the job.
    97123
    98124**Link reading (optional, off by default)** — when you enable "Let the bot read links
    … …  
    154180permission_callback, and the endpoints the AI uses as tools are internal-only -
    155181they cannot be reached from outside the site. Visitor sessions use hashed tokens
    156 and chat messages are rate limited per session.
     182and chat messages are rate limited per session. When a background AI reply is started
     183through a loopback request, that request is signed and expires after 60 seconds.
     184
     185The plugin server never changes a shopper's cart: "Add to cart" in the chat calls
     186the WooCommerce Store API from the shopper's own browser. The AI can only propose
     187products that come from your catalog, and the card always reads the current price
     188and stock from WooCommerce.
    157189
    158190Chat messages are rendered as DOM elements rather than injected HTML, and only
    … …  
    178210paragraph about this to Settings > Privacy.
    179211
     212Sales Insights stores the questions the assistant could not answer, with email addresses
     213and phone numbers hidden, and in the Starter plan the reason a chat did not lead to an
     214order with a short quote. When a logged-in customer chats, or a customer proves they own
     215an order, the conversation records that account or order number, so your team can see
     216the customer card (Starter plan). The language of each conversation is stored to reply in
     217it. All of this is included in the personal-data export and removed by the eraser.
     218
     219In the visitor's browser, the chat keeps its session token and, for the new-replies badge,
     220the conversation number, the last message seen and when it was seen (localStorage). No
     221message text is stored there.
     222
    180223== Installation ==
    181224
    … …  
    192235AI provider for your own API key, or use local-only mode with no key at all.
    193236
     237= Can this WooCommerce chatbot add products to the cart? =
     238
     239Yes. Product answers show cards with an Add to cart button, and the cart is updated by WooCommerce
     240in the shopper's browser, so your prices, stock and cart rules apply. In the free version, products
     241with options link to the product page; the Starter plan lets shoppers choose options and quantities
     242on the card. Nothing is added until the shopper taps the button.
     243
    194244= Can customers track their orders in the chat? =
    195245
    196 Yes. When a customer gives the order number together with the email address or phone number used
    197 for the order, the assistant shows the order status in the chat. Without that proof it reveals
    198 nothing about the order.
     246Yes. The assistant shows an order card with the status, items and tracking number. Logged-in
     247customers can ask for their recent orders. Guests give the order number together with the email
     248address or phone number used for the order; without that proof the assistant reveals nothing.
     249
     250= Does it reply in the customer's language? =
     251
     252Yes, by default on new installs. It detects the language of each message and answers in it. Sites
     253upgraded from an earlier version keep replying in the store's language until you change "Reply
     254language" in Settings.
    199255
    200256= Which AI models does the chatbot work with? =
    201257
    202 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. You choose the
     258Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. On WordPress 7.0 and
     259later you can also use the AI provider connected in Settings > Connectors. You choose the
    203260provider and can switch at any time.
    204261
     262= What does Sales Insights show? =
     263
     264The questions shoppers asked that the assistant could not answer, grouped and ranked, what they
     265searched for but could not find, and which products they wanted while out of stock. You can add
     266an answer from the same screen. The Starter plan adds the reasons chats did not lead to an order and a
     267weekly email summary.
     268
     269= Can AI agents read my product catalog (MCP)? =
     270
     271Yes, if you allow it. On WordPress 6.9 and later, turn on "AI agents" in Settings: the plugin
     272registers read-only abilities for product search, product details and store policies. With the
     273MCP Adapter plugin, MCP clients can use them. Orders, carts and customer data are never shared.
     274
    205275= Does the shopping assistant recommend products? =
    206276
    207277Yes. It searches your synced WooCommerce catalog, recommends products that match what the shopper
    208 asked for and links to them. It only recommends products that exist in your store.
     278asked for and shows them as cards. It only recommends products that exist in your store.
    209279
    210280= Do I need a paid AI subscription? =
    … …  
    213283and are billed directly by that provider. Local-only mode needs no key.
    214284
    215 = How much does the AI cost to run? =
    216 
    217 It depends on the provider and model you choose, and you pay the provider directly. In our
    218 internal test of 60 conversations with GPT-4o mini, the AI cost about US$0.0006 per conversation;
    219 larger models cost more. The AI Value & Performance tab shows what your own store actually spent.
     285= How much does an AI chatbot for WooCommerce cost to run? =
     286
     287It depends on the provider and model you choose, and you pay the provider directly. In our test
     288of 60 conversations on a real store with version 2.6.0, GPT-4o mini cost about US$0.0009 per
     289conversation and Gemini 3.8 Flash about US$0.015. The AI Value & Performance tab shows what your
     290own store actually spent.
    220291
    221292= Can I try it without an API key? =
    222293
    223 Yes. Local-only mode answers product, cart and order questions from your store data without
    224 calling any AI provider. Add a key later when you want full conversations.
     294Yes. Local-only mode answers greetings, store policies, cart and order questions from your store
     295data without calling any AI provider. Add a key later when you want full conversations.
    225296
    226297= Will it slow down my store? =
    227298
    228 The chat button is small, and the chat window and voice input load only when a visitor opens
    229 the chat. Nothing is loaded from third-party servers unless you pick a Google font.
     299The chat button is small. The chat window, product cards, cart view and voice input load only
     300when they are needed, and AI replies run in the background after a message is sent. While the
     301chat is minimized after a recent conversation, it checks for new replies every 25 seconds, only
     302while the browser tab is visible. Nothing is loaded from third-party servers unless you pick a
     303Google font.
    230304
    231305= Is my store data sent to you? =
    … …  
    237311= Does it work without WooCommerce? =
    238312
    239 The chat and lead capture work on any WordPress site. Product search, catalog grounding and
    240 order lookup require WooCommerce.
     313The chat and lead capture work on any WordPress site. Product search, product cards, catalog
     314grounding and order lookup require WooCommerce.
    241315
    242316= Will it invent prices or stock? =
    243317
    244 No. The playbook forbids it and product facts come from lookups against your store. If the
    245 catalog has not been synced, the bot is told to say so and offer a handoff instead of guessing.
     318No. Product facts come from lookups against your store, and product cards read the current price
     319and stock from WooCommerce when the reply is shown. If the catalog has not been synced, the bot
     320is told to say so and offer a handoff instead of guessing.
    246321
    247322= What counts as an "AI-assisted order"? =
    … …  
    249324An order placed within your attribution window (24 hours, 72 hours or 7 days) after the
    250325customer received an answer with products from your store, or clicked a product or cart link
    251 in the chat. Each order is counted once. Cancelled, failed and fully refunded orders are left
    252 out, and partial refunds are deducted. AI-assisted does not mean the AI was the only reason
    253 for the order.
     326in the chat, or added a product to the cart from the chat. Each order is counted once.
     327Cancelled, failed and fully refunded orders are left out, and partial refunds are deducted.
     328AI-assisted does not mean the AI was the only reason for the order.
    254329
    255330= Is anything in the free version locked? =
    … …  
    274349
    275350== Changelog ==
     351
     352= 2.6.0 =
     353* Added: add to cart in the chat. Product answers show cards with photo, current price, stock and rating, read from WooCommerce when the reply is shown. Simple products have an Add to cart button that uses the WooCommerce Store API in the shopper's browser; the plugin server never changes the cart. A cart view in the chat shows the items, subtotal and Checkout.
     354* Added: order tracking cards in the chat, with status, date, items, total and tracking number with a Track link (WooCommerce Shipment Tracking, Advanced Shipment Tracking or an order field you choose in Settings). Logged-in customers can ask for their recent orders without giving an email address.
     355* Added: Sales Insights tab: questions the assistant could not answer, grouped and ranked, with "Add answer" to teach the assistant; searches with no matching product; products wanted while out of stock. Email addresses and phone numbers are hidden.
     356* Added: replies in the shopper's language (15 languages detected without an AI call). New installs reply in the customer's language; upgraded sites keep the store's language until you change "Reply language" in Settings.
     357* Added (Starter): choose options and quantities on product cards, remove items and apply discount codes you allow; the assistant can prepare the card with the shopper's choice.
     358* Added (Starter): nightly AI labels of why chats did not lead to an order, with quotes, a monthly spending limit and an optional weekly email summary.
     359* Added (Starter): a customer card in the Inbox for logged-in customers and customers who proved an order: order count, total spent, last order and recent products. Shown only to users who can view WooCommerce orders.
     360* Added: a badge on the chat button when a reply arrives while the chat is minimized.
     361* Added: "WordPress AI connection" as an AI provider on WordPress 7.0 and later (Settings > Connectors).
     362* Added: optional read-only abilities for AI agents (WordPress Abilities API, WordPress 6.9+): product search, product details and store policies. Off by default.
     363* Added: after a week of use, once the assistant has answered at least 10 conversations or helped with an order, administrators are asked on the plugin screens whether they would leave a review on WordPress.org, with "Maybe later" and "Don't ask again". At most three times, never within a day of an AI error, and it can be turned off with a filter.
     364* Changed: AI replies now run in the background after a message is sent, instead of inside the visitor's next request. At most 2 replies run at the same time by default. The chat checks for replies often right after a message and less often while idle.
     365* Changed: product search also finds products when the shopper's words are in a different order or include extra words.
     366* Changed: the plugin is now named "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce".
     367* Fixed: products hidden from the catalog could still be recommended.
     368* Fixed: a product on sale for 0 was quoted at its regular price, and a sale scheduled for later was quoted before it started.
     369* Fixed: questions that mention payment (for example "Do you have a plugin for QR payments?") were answered with the checkout link.
     370* Fixed: logged-in customers were treated as guests in the chat, so their account could not be used to look up their orders.
     371* Fixed: with Google Gemini, a saved model that Google no longer offers to new API keys made every reply fail. The plugin now falls back to the current Gemini Flash model and logs a reminder to update the setting.
     372* Fixed: Gemini could return an empty reply when its thinking used up the answer length.
     373* Fixed: with LiteSpeed Cache, the cart shown in the chat could be served from the page cache and look empty or out of date.
     374* Fixed: when the AI provider was slow, the visitor could be told the assistant was still looking and then get no answer.
    276375
    277376= 2.5.0 =
    … …  
    370469== Upgrade Notice ==
    371470
     471= 2.6.0 =
     472Add to cart in the chat, order tracking with tracking numbers, replies in the shopper's language and Sales Insights. Data and settings are kept; replies stay in your store's language until you change "Reply language". The catalog refreshes within the hour.
     473
    372474= 2.5.0 =
    373475Replies display with proper formatting and product images no longer break. Adds optional voice input (off by default). Your data and settings are kept. To refresh product images right away, click "Sync products now" in Settings; otherwise they refresh within the hour.
    … …  
    378480= 2.4.0 =
    379481New AI Value & Performance tab and a redesigned chat widget with a "Talk to a person" button. Statistics start on the day you upgrade. Check Settings > Business information: sample text from earlier versions is no longer used, so enter your real details.
    380 
    381 = 2.3.0 =
    382 Telegram notifications and other paid features now ship only in the Starter and Pro plans. Adds personal-data export/erasure and WooCommerce HPOS compatibility.
  • ninhle-ai-sales-assistant/trunk/src/JS/admin/components/Inbox.tsx

    r3761175 r3765552  
    22import { api } from '../api';
    33import { ConfirmBox, NoticeList, useConfirm, useNotices } from './Feedback';
     4import { CustomerCard, CustomerCardData } from './CustomerCard';
    45
    56// Nhãn hiển thị (2.5.0): chủ shop đọc "Waiting for your team" thay vì mã human_active.
    … …  
    2425};
    2526
    26 export function Inbox() {
     27/** openId: mở thẳng hội thoại này (link "Open chat" trong Sales Insights). */
     28export function Inbox({ openId = null }: { openId?: number | null } = {}) {
    2729    const { notices, push, dismiss } = useNotices();
    2830    const confirmation = useConfirm();
    … …  
    3739    const timeoutRef = useRef<number | null>(null);
    3840    const lastMessageIdRef = useRef<number | null>(null);
     41    // Thẻ khách (S12, Starter): chỉ có khi khách đã biết chắc; tải lại khi có tin mới (khách vừa chứng minh đơn).
     42    const [customer, setCustomer] = useState<CustomerCardData | null>(null);
     43    const shownIdRef = useRef<number | null>(null);
     44    const loadCustomer = (id: number) => {
     45        // Trả lời muộn của hội thoại trước không được hiện trên hội thoại đang mở.
     46        api.get(`/admin/conversations/${id}/customer`)
     47            .then(data => { if (shownIdRef.current === id) setCustomer(data?.card || null); })
     48            .catch(() => { if (shownIdRef.current === id) setCustomer(null); });
     49    };
    3950
    4051    useEffect(() => { loadConversations(); }, []);
     52
     53    useEffect(() => {
     54        if (!openId) return;
     55        api.get(`/admin/conversations/${openId}`)
     56            .then(conv => conv && selectConversation(conv))
     57            .catch(() => push('error', 'This conversation no longer exists.'));
     58    }, [openId]);
    4159
    4260    const loadConversations = async () => {
    … …  
    4967    const selectConversation = async (conv: any) => {
    5068        setActiveConv(conv);
     69        shownIdRef.current = conv.id;
     70        setCustomer(null);
     71        loadCustomer(conv.id);
    5172        try {
    5273            const data = await api.getMessages(conv.id);
    … …  
    6889                scrollToBottom();
    6990                pollIntervalRef.current = 5000;
     91                loadCustomer(activeConv.id);
    7092            }
    7193        } catch { pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000); }
    … …  
    86108            try {
    87109                await api.deleteConversation(id);
    88                 if (activeConv?.id === id) { setActiveConv(null); setMessages([]); }
     110                if (activeConv?.id === id) { setActiveConv(null); setMessages([]); setCustomer(null); shownIdRef.current = null; }
    89111                setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; });
    90112                loadConversations();
    … …  
    111133            try {
    112134                const result = await api.bulkDeleteConversations(Array.from(selectedIds));
    113                 if (activeConv && selectedIds.has(activeConv.id)) { setActiveConv(null); setMessages([]); }
     135                if (activeConv && selectedIds.has(activeConv.id)) { setActiveConv(null); setMessages([]); setCustomer(null); shownIdRef.current = null; }
    114136                setSelectedIds(new Set());
    115137                loadConversations();
    … …  
    197219                        </div>
    198220                    </div>
     221                    {customer && (
     222                        <details className="smb-customer-inline">
     223                            <summary>Customer: {customer.name || 'Guest customer'}</summary>
     224                            <CustomerCard card={customer} />
     225                        </details>
     226                    )}
    199227                    <div className="smb-commerce-chat-history">
    200228                        {messages.map(m => (
    … …  
    221249                )}
    222250            </div>
     251            {activeConv && customer && (
     252                <aside className="smb-customer-aside" aria-label="Customer">
     253                    <CustomerCard card={customer} />
     254                </aside>
     255            )}
    223256        </div>
    224257        </>
  • ninhle-ai-sales-assistant/trunk/src/JS/admin/components/Settings.tsx

    r3761175 r3765552  
    3838        smbcommerce_hide_on_checkout: '0',
    3939        smbcommerce_voice_input_enabled: '0',
     40        smbcommerce_reply_language: 'customer',
     41        smbcommerce_coupon_allowlist: '',
     42        smbcommerce_tracking_code_meta: '',
     43        smbcommerce_tracking_carrier_meta: '',
     44        smbcommerce_tracking_carrier: '',
     45        smbcommerce_tracking_url_template: '',
     46        smbcommerce_abilities_enabled: '0',
    4047        smbcommerce_gemini_model: '',
    4148        smbcommerce_anthropic_model: '',
    … …  
    7481    const [features, setFeatures] = useState<any>({});
    7582    const [premiumBuild, setPremiumBuild] = useState(true);
     83    // Kết nối AI của WordPress 7.0+ (2.6.0, S7): máy chủ chỉ báo có/không.
     84    const [wpAiAvailable, setWpAiAvailable] = useState(false);
     85    // Abilities API cho AI agent (2.6.0, S8): WordPress 6.9+.
     86    const [abilitiesSupported, setAbilitiesSupported] = useState(false);
    7687    const [modelReport, setModelReport] = useState<any>(null);
    7788    // Thông tin kinh doanh còn nguyên đoạn mẫu bản cũ tự nạp: bot không dùng cho tới khi chủ shop sửa.
    … …  
    179190                    smbcommerce_hide_on_checkout: data.settings.smbcommerce_hide_on_checkout ?? '0',
    180191                    smbcommerce_voice_input_enabled: data.settings.smbcommerce_voice_input_enabled ?? '0',
     192                    smbcommerce_reply_language: data.settings.smbcommerce_reply_language || 'customer',
     193                    smbcommerce_coupon_allowlist: data.settings.smbcommerce_coupon_allowlist || '',
     194                    smbcommerce_tracking_code_meta: data.settings.smbcommerce_tracking_code_meta || '',
     195                    smbcommerce_tracking_carrier_meta: data.settings.smbcommerce_tracking_carrier_meta || '',
     196                    smbcommerce_tracking_carrier: data.settings.smbcommerce_tracking_carrier || '',
     197                    smbcommerce_tracking_url_template: data.settings.smbcommerce_tracking_url_template || '',
     198                    smbcommerce_abilities_enabled: data.settings.smbcommerce_abilities_enabled ?? '0',
    181199                    smbcommerce_gemini_model: data.settings.smbcommerce_gemini_model || '',
    182200                    smbcommerce_anthropic_model: data.settings.smbcommerce_anthropic_model || '',
    … …  
    199217                setFeatures(data.settings.smbcommerce_features || {});
    200218                setPremiumBuild(data.settings.smbcommerce_premium_build !== false);
     219                setWpAiAvailable(data.settings.smbcommerce_wp_ai_available === true);
     220                setAbilitiesSupported(data.settings.smbcommerce_abilities_supported === true);
    201221                setModelReport(data.settings.smbcommerce_model_report || null);
    202222                setSampleBusinessInfo(data.settings.smbcommerce_business_info_is_sample ? data.settings.smbcommerce_business_info : null);
    … …  
    247267                smbcommerce_hide_on_checkout: settings.smbcommerce_hide_on_checkout,
    248268                smbcommerce_voice_input_enabled: settings.smbcommerce_voice_input_enabled,
     269                smbcommerce_reply_language: settings.smbcommerce_reply_language,
     270                smbcommerce_coupon_allowlist: settings.smbcommerce_coupon_allowlist,
     271                smbcommerce_tracking_code_meta: settings.smbcommerce_tracking_code_meta,
     272                smbcommerce_tracking_carrier_meta: settings.smbcommerce_tracking_carrier_meta,
     273                smbcommerce_tracking_carrier: settings.smbcommerce_tracking_carrier,
     274                smbcommerce_tracking_url_template: settings.smbcommerce_tracking_url_template,
     275                smbcommerce_abilities_enabled: settings.smbcommerce_abilities_enabled,
    249276                smbcommerce_gemini_model: settings.smbcommerce_gemini_model,
    250277                smbcommerce_anthropic_model: settings.smbcommerce_anthropic_model,
    … …  
    562589
    563590                            <div className="smb-commerce-form-group">
     591                                <label htmlFor="smb-reply-language">Reply language</label>
     592                                <select
     593                                    id="smb-reply-language"
     594                                    value={settings.smbcommerce_reply_language}
     595                                    onChange={e => setSettings({...settings, smbcommerce_reply_language: e.target.value})}
     596                                >
     597                                    <option value="customer">The customer's language (recommended)</option>
     598                                    <option value="store">As in earlier versions: follow the sales playbook and site language</option>
     599                                </select>
     600                                <p className="description">
     601                                    With the customer's language, the assistant replies in the language each
     602                                    customer writes in and keeps product names, prices and order numbers exactly
     603                                    as they are in your store. Fixed messages, such as the handoff confirmation,
     604                                    follow the customer's language when this site has a translation for it.
     605                                </p>
     606                            </div>
     607
     608                            <div className="smb-commerce-form-group">
    564609                                <label>AI provider</label>
    565610                                <select
    … …  
    570615                                    <option value="anthropic">Anthropic (Claude 3.5)</option>
    571616                                    <option value="gemini">Google Gemini</option>
     617                                    {(wpAiAvailable || settings.smbcommerce_ai_provider === 'wordpress') && (
     618                                        <option value="wordpress">WordPress AI connection (Settings → Connectors)</option>
     619                                    )}
    572620                                </select>
     621                                {settings.smbcommerce_ai_provider === 'wordpress' && (
     622                                    <p className="description">
     623                                        {wpAiAvailable
     624                                            ? 'Uses the AI provider you set up in Settings → Connectors. The key stays in WordPress; this plugin never reads it. The keys below are not needed for this option.'
     625                                            : 'No AI provider is set up in Settings → Connectors any more. Set one up there, or choose another provider here.'}
     626                                    </p>
     627                                )}
    573628                            </div>
    574629
    … …  
    10411096
    10421097                        <div className="smb-commerce-card">
     1098                            <h3><span className="dashicons dashicons-cart"></span> Cart in the chat</h3>
     1099                            <p className="description" style={{marginTop: 0}}>
     1100                                Product cards in the chat have an Add to cart button and the chat shows the customer's
     1101                                cart. Everything goes through your store's own cart, so prices, stock and coupon rules
     1102                                are checked by WooCommerce as usual. The assistant never changes a cart by itself.
     1103                            </p>
     1104                            {!allowed('cart_actions_advanced') ? (
     1105                                <p className="description" id="smb-commerce-cart-plan-note">
     1106                                    Choosing product options on the card, changing quantities, removing items and applying
     1107                                    discount codes in the chat are available in the Starter plan.
     1108                                </p>
     1109                            ) : (
     1110                                <div className="smb-commerce-form-group">
     1111                                    <label htmlFor="smb-coupon-allowlist">Discount codes the assistant may offer</label>
     1112                                    <textarea
     1113                                        id="smb-coupon-allowlist"
     1114                                        rows={4}
     1115                                        placeholder={'WELCOME10, 10\nFREESHIP'}
     1116                                        value={settings.smbcommerce_coupon_allowlist}
     1117                                        onChange={e => setSettings({...settings, smbcommerce_coupon_allowlist: e.target.value})}
     1118                                    />
     1119                                    <p className="description">
     1120                                        One coupon code per line, up to 20. Add a comma and a number to cap the discount the
     1121                                        assistant may offer: a percentage for percentage coupons, an amount for fixed
     1122                                        coupons. The assistant offers a code only when a customer asks about discounts or
     1123                                        hesitates over the price, and shows an Apply button. Codes that are not listed here
     1124                                        are never offered. Leave it empty to turn this off.
     1125                                    </p>
     1126                                </div>
     1127                            )}
     1128                        </div>
     1129
     1130                        {abilitiesSupported && (
     1131                            <div className="smb-commerce-card">
     1132                                <h3><span className="dashicons dashicons-rest-api"></span> AI agents (Abilities API / MCP)</h3>
     1133                                <div className="smb-commerce-form-group toggle-group">
     1134                                    <label className="toggle-switch">
     1135                                        <input type="checkbox" checked={settings.smbcommerce_abilities_enabled === '1'}
     1136                                            onChange={e => setSettings({...settings, smbcommerce_abilities_enabled: e.target.checked ? '1' : '0'})} />
     1137                                        <span className="toggle-slider"></span>
     1138                                    </label>
     1139                                    <div>
     1140                                        <strong>Let AI agents read your public catalog</strong>
     1141                                        <p className="description" style={{margin: 0}}>
     1142                                            Adds three read-only abilities to WordPress: search products, product details and
     1143                                            store policies. With the MCP Adapter plugin installed, AI assistants and agents can
     1144                                            use them as tools. Only published, visible products and your policy texts are
     1145                                            shared; never orders, carts or customer data. Limited to 60 requests per hour per
     1146                                            visitor. Off by default.
     1147                                        </p>
     1148                                    </div>
     1149                                </div>
     1150                            </div>
     1151                        )}
     1152
     1153                        <div className="smb-commerce-card">
     1154                            <h3><span className="dashicons dashicons-location"></span> Order tracking</h3>
     1155                            <p className="description" style={{marginTop: 0}}>
     1156                                When a customer asks about an order, the chat shows an order card with its status and
     1157                                tracking number. Tracking numbers saved by WooCommerce Shipment Tracking or Advanced
     1158                                Shipment Tracking appear automatically. If your shipping plugin saves the tracking number
     1159                                in its own order field, enter that field's meta key below. Logged-in customers can ask
     1160                                about their orders without an order number.
     1161                            </p>
     1162                            <div className="smb-commerce-form-group">
     1163                                <label htmlFor="smb-tracking-code-meta">Tracking number meta key</label>
     1164                                <input id="smb-tracking-code-meta" type="text" placeholder="e.g. _ghn_order_code"
     1165                                    value={settings.smbcommerce_tracking_code_meta}
     1166                                    onChange={e => setSettings({...settings, smbcommerce_tracking_code_meta: e.target.value})} />
     1167                            </div>
     1168                            <div className="smb-commerce-form-group">
     1169                                <label htmlFor="smb-tracking-carrier-meta">Carrier meta key (optional)</label>
     1170                                <input id="smb-tracking-carrier-meta" type="text"
     1171                                    value={settings.smbcommerce_tracking_carrier_meta}
     1172                                    onChange={e => setSettings({...settings, smbcommerce_tracking_carrier_meta: e.target.value})} />
     1173                            </div>
     1174                            <div className="smb-commerce-form-group">
     1175                                <label htmlFor="smb-tracking-carrier">Carrier name (optional)</label>
     1176                                <input id="smb-tracking-carrier" type="text" placeholder="e.g. GHN"
     1177                                    value={settings.smbcommerce_tracking_carrier}
     1178                                    onChange={e => setSettings({...settings, smbcommerce_tracking_carrier: e.target.value})} />
     1179                                <p className="description">Used when there is no carrier meta key, or it is empty on an order.</p>
     1180                            </div>
     1181                            <div className="smb-commerce-form-group">
     1182                                <label htmlFor="smb-tracking-url">Tracking link (optional)</label>
     1183                                <input id="smb-tracking-url" type="url" placeholder="https://carrier.example/track?code={code}"
     1184                                    value={settings.smbcommerce_tracking_url_template}
     1185                                    onChange={e => setSettings({...settings, smbcommerce_tracking_url_template: e.target.value})} />
     1186                                <p className="description">
     1187                                    A web address with {'{code}'} where the tracking number goes. The chat shows a Track
     1188                                    button only when this is set. Meta keys may use letters, numbers, _ and -.
     1189                                </p>
     1190                            </div>
     1191                        </div>
     1192
     1193                        <div className="smb-commerce-card">
    10431194                            <h3><span className="dashicons dashicons-admin-tools"></span> Advanced features</h3>
    10441195
  • ninhle-ai-sales-assistant/trunk/src/JS/admin/index.tsx

    r3752682 r3765552  
    1 import React, { useState } from 'react';
     1import React, { useEffect, useState } from 'react';
    22import { createRoot } from 'react-dom/client';
    33import { Inbox } from './components/Inbox';
    … …  
    66import { Settings } from './components/Settings';
    77import { ValueDashboard } from './components/ValueDashboard';
     8import { loadInsights } from './insightsLoader';
     9import { t } from './i18n';
    810import './style.css';
    911
    10 const TABS = ['value', 'inbox', 'rules', 'leads'];
     12const TABS = ['value', 'insights', 'inbox', 'rules', 'leads'];
    1113const TAB_KEY = 'smbcommerce_admin_tab';
    1214
    13 /** Tab đã mở lần trước (chỉ là tiện ích trên trình duyệt này; lỗi bộ nhớ thì về tab đầu). */
     15/** Tab mở đầu: ?smb_tab= (link trong bản tin Sales Insights), rồi tab lần trước trên trình duyệt này. */
    1416function initialTab() {
     17  const fromUrl = new URLSearchParams(window.location.search).get('smb_tab');
     18  if (fromUrl && TABS.includes(fromUrl)) return fromUrl;
    1519  try {
    1620    const saved = window.localStorage.getItem(TAB_KEY);
    … …  
    2327const AdminApp = () => {
    2428  const [activeTab, setActiveTabState] = useState(initialTab);
     29  const [insightsMod, setInsightsMod] = useState<any>(null);
     30  const [insightsFailed, setInsightsFailed] = useState(false);
     31  const [inboxOpenId, setInboxOpenId] = useState<number | null>(null);
    2532  const setActiveTab = (tab: string) => {
    2633    setActiveTabState(tab);
    … …  
    2835  };
    2936  const valueTitle = (window as any).SMBCommerceAdmin?.i18n?.tab || 'AI Value & Performance';
     37  const insightsTitle = (window as any).SMBCommerceAdmin?.i18n?.insightsTab || 'Sales Insights';
     38
     39  // Sales Insights (2.6.0): module riêng, chỉ tải khi mở tab.
     40  useEffect(() => {
     41    if (activeTab !== 'insights' || insightsMod) return;
     42    setInsightsFailed(false);
     43    loadInsights().then(mod => setInsightsMod(() => mod)).catch(() => setInsightsFailed(true));
     44  }, [activeTab, insightsMod]);
     45
     46  const openConversation = (id: number) => {
     47    setInboxOpenId(id);
     48    setActiveTab('inbox');
     49  };
     50  const tab = (key: string, label: string) => (
     51    <button className={`nav-tab ${activeTab === key ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab(key)} aria-current={activeTab === key ? 'page' : undefined}>{label}</button>
     52  );
    3053
    3154  return (
    … …  
    3356      <h1>Ninhle AI Sales Assistant Admin</h1>
    3457      <h2 className="nav-tab-wrapper">
    35         <button className={`nav-tab ${activeTab === 'value' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('value')} aria-current={activeTab === 'value' ? 'page' : undefined}>{valueTitle}</button>
    36         <button className={`nav-tab ${activeTab === 'inbox' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('inbox')} aria-current={activeTab === 'inbox' ? 'page' : undefined}>Inbox</button>
    37         <button className={`nav-tab ${activeTab === 'rules' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('rules')} aria-current={activeTab === 'rules' ? 'page' : undefined}>Rules Engine</button>
    38         <button className={`nav-tab ${activeTab === 'leads' ? 'nav-tab-active' : ''}`} onClick={() => setActiveTab('leads')} aria-current={activeTab === 'leads' ? 'page' : undefined}>Leads</button>
     58        {tab('value', valueTitle)}
     59        {tab('insights', insightsTitle)}
     60        {tab('inbox', 'Inbox')}
     61        {tab('rules', 'Rules Engine')}
     62        {tab('leads', 'Leads')}
    3963      </h2>
    4064      <div style={{ marginTop: 20 }}>
    4165          {activeTab === 'value' && <ValueDashboard />}
    42           {activeTab === 'inbox' && <Inbox />}
     66          {activeTab === 'insights' && (insightsMod
     67            ? <insightsMod.SalesInsights onOpenConversation={openConversation} />
     68            : insightsFailed
     69              ? <div className="notice notice-error smb-inline-notice" role="alert"><p>{t('loadError')}</p></div>
     70              : <p role="status" className="smb-value-status">{t('loading')}</p>)}
     71          {activeTab === 'inbox' && <Inbox openId={inboxOpenId} />}
    4372          {activeTab === 'rules' && <Rules />}
    4473          {activeTab === 'leads' && <Leads />}
  • ninhle-ai-sales-assistant/trunk/src/JS/admin/style.css

    r3752682 r3765552  
    374374}
    375375
     376/* Thẻ khách trong Inbox (2.6.0, S12): cột phải; màn hẹp thì thành khối thu gọn trên khung chat. */
     377.smb-customer-aside {
     378    width: 260px;
     379    flex-shrink: 0;
     380    border-left: 1px solid #ccd0d4;
     381    overflow-y: auto;
     382    background: #fff;
     383    padding: 14px;
     384}
     385.smb-customer-inline {
     386    display: none;
     387    border-bottom: 1px solid #ccd0d4;
     388    padding: 8px 15px;
     389    background: #f6f7f7;
     390}
     391.smb-customer-inline summary {
     392    cursor: pointer;
     393    font-weight: 600;
     394}
     395.smb-customer-card h4 {
     396    margin: 0 0 8px;
     397    font-size: 14px;
     398}
     399.smb-customer-card dl {
     400    margin: 0;
     401}
     402.smb-customer-card dt {
     403    margin-top: 10px;
     404    color: #646970;
     405    font-size: 12px;
     406}
     407.smb-customer-card dd {
     408    margin: 2px 0 0;
     409    overflow-wrap: anywhere;
     410}
     411.smb-customer-card ul {
     412    margin: 0;
     413    padding-left: 16px;
     414    list-style: disc;
     415}
     416.smb-customer-sub,
     417.smb-customer-note {
     418    color: #646970;
     419    font-size: 12px;
     420}
     421.smb-customer-links {
     422    margin: 12px 0 0;
     423}
     424.smb-customer-warn {
     425    margin: 0 0 8px;
     426    padding: 6px 8px;
     427    background: #fcf9e8;
     428    border-left: 3px solid #dba617;
     429    font-size: 12px;
     430}
     431@media (max-width: 1100px) {
     432    .smb-customer-aside {
     433        display: none;
     434    }
     435    .smb-customer-inline {
     436        display: block;
     437    }
     438}
     439
    376440.smb-commerce-main-input {
    377441    padding: 15px;
    … …  
    656720    outline-offset: 2px;
    657721}
     722
     723/* Sales Insights (2.6.0, S6) */
     724.smb-insights-grid {
     725    display: grid;
     726    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
     727    gap: 16px;
     728    align-items: start;
     729}
     730
     731.smb-insights-wide {
     732    grid-column: 1 / -1;
     733}
     734
     735.smb-insights-table td {
     736    vertical-align: top;
     737}
     738
     739.smb-insights-table td:nth-child(2),
     740.smb-insights-table td:nth-child(3) {
     741    white-space: nowrap;
     742}
     743
     744.smb-insights-table td strong {
     745    overflow-wrap: anywhere;
     746}
     747
     748.smb-insights-actions {
     749    display: flex;
     750    flex-wrap: wrap;
     751    gap: 6px 12px;
     752    align-items: center;
     753    justify-content: flex-end;
     754}
     755
     756.smb-insights-tags {
     757    display: flex;
     758    flex-wrap: wrap;
     759    gap: 4px;
     760    margin-top: 4px;
     761}
     762
     763.smb-insights-tag {
     764    padding: 1px 8px;
     765    border-radius: 999px;
     766    background: #f0f0f1;
     767    color: #50575e;
     768    font-size: 11px;
     769}
     770
     771.smb-insights-draft form {
     772    display: grid;
     773    gap: 8px;
     774    max-width: 720px;
     775}
     776
     777.smb-insights-draft label {
     778    display: grid;
     779    gap: 4px;
     780    font-weight: 600;
     781}
     782
     783.smb-insights-draft fieldset {
     784    margin: 0;
     785    padding: 0;
     786    border: 0;
     787}
     788
     789.smb-insights-draft fieldset label {
     790    display: flex;
     791    font-weight: 400;
     792}
     793
     794.smb-insights-notes ul {
     795    margin: 0;
     796}
     797
     798.smb-insights-notes li {
     799    padding: 6px 0;
     800    border-top: 1px solid #f0f0f1;
     801}
     802
     803.smb-insights-list li {
     804    display: flex;
     805    justify-content: space-between;
     806    gap: 12px;
     807    padding: 6px 0;
     808    border-top: 1px solid #f0f0f1;
     809}
     810
     811.smb-insights-list li > span:first-child {
     812    min-width: 0;
     813    overflow-wrap: anywhere;
     814}
     815
     816.smb-insights-list li > span:last-child {
     817    flex-shrink: 0;
     818    text-align: right;
     819}
     820
     821.smb-insights-list a,
     822.smb-insights-list .button-link,
     823.smb-insights-actions .button-link {
     824    white-space: nowrap;
     825}
     826
     827.smb-insights-why {
     828    margin: 0;
     829}
     830
     831.smb-insights-why li {
     832    padding: 8px 0;
     833    border-top: 1px solid #f0f0f1;
     834}
     835
     836.smb-insights-why-row {
     837    display: flex;
     838    justify-content: space-between;
     839    gap: 12px;
     840}
     841
     842.smb-insights-bar {
     843    height: 6px;
     844    margin-top: 4px;
     845    border-radius: 999px;
     846    background: #f0f0f1;
     847    overflow: hidden;
     848}
     849
     850.smb-insights-bar span {
     851    display: block;
     852    height: 100%;
     853    background: #2271b1;
     854}
     855
     856.smb-insights-quote {
     857    margin: 4px 0 0;
     858    padding-left: 8px;
     859    border-left: 3px solid #dcdcde;
     860    color: #50575e;
     861    font-size: 12px;
     862    overflow-wrap: anywhere;
     863}
     864
     865@media (max-width: 782px) {
     866    .smb-insights-table thead {
     867        display: none;
     868    }
     869    .smb-insights-table tr,
     870    .smb-insights-table td {
     871        display: block;
     872        width: auto;
     873    }
     874    .smb-insights-table td[data-label]::before {
     875        content: attr(data-label) ": ";
     876        color: #646970;
     877    }
     878    .smb-insights-table td.smb-insights-actions {
     879        display: flex;
     880        justify-content: flex-start;
     881    }
     882    .smb-insights-list li > span:last-child {
     883        flex-shrink: 1;
     884    }
     885}
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/api.ts

    r3752682 r3765552  
    4040    },
    4141
     42    /**
     43     * Gửi tin. Khách đã đăng nhập (2.6.0, S5): kèm nonce REST để máy chủ biết tài khoản ("đơn của tôi" không
     44     * cần mã đơn). Nonce bị từ chối (trang cache cũ, phiên đăng nhập đã đổi) thì gửi lại như khách vãng lai.
     45     */
    4246    async sendMessage(conversationId: number, content: string, clientMessageId: string) {
    43         const res = await fetch(`${BASE_URL}/chat/message`, {
     47        const body = JSON.stringify({
     48            conversation_id: conversationId,
     49            content: content,
     50            client_message_id: clientMessageId
     51        });
     52        const w = window.SMBCommerceWidget;
     53        const send = (withNonce: boolean) => fetch(`${BASE_URL}/chat/message`, {
    4454            method: 'POST',
    45             headers: this.getHeaders(),
    46             body: JSON.stringify({
    47                 conversation_id: conversationId,
    48                 content: content,
    49                 client_message_id: clientMessageId
    50             })
     55            headers: withNonce ? { ...this.getHeaders(), 'X-WP-Nonce': String(w.nonce) } : this.getHeaders(),
     56            body
    5157        });
     58        const withNonce = !!(w && w.loggedIn && w.nonce);
     59        let res = await send(withNonce);
     60        if (withNonce && res.status === 403) {
     61            const err = await res.clone().json().catch(() => ({} as any));
     62            if (err && err.code === 'rest_cookie_invalid_nonce') res = await send(false);
     63        }
    5264        if (!res.ok) throw new Error('Failed to send message');
    5365        return await res.json();
    … …  
    6981     * request vẫn đi khi khách vừa bấm link rời trang; lỗi bỏ qua vì không ảnh hưởng tới chat.
    7082     */
    71     event(conversationId: number, type: string, ref = '') {
     83    event(conversationId: number, type: string, ref = '', extra: Record<string, number> = {}) {
    7284        fetch(`${BASE_URL}/chat/event`, {
    7385            method: 'POST',
    7486            headers: this.getHeaders(),
    75             body: JSON.stringify({ conversation_id: conversationId, type, ref }),
     87            body: JSON.stringify({ conversation_id: conversationId, type, ref, ...extra }),
    7688            keepalive: true
    7789        }).catch(() => { /* đo lường không được làm hỏng chat */ });
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/components/ChatButton.tsx

    r3761175 r3765552  
    88    config: any;
    99    buttonRef?: Ref<HTMLButtonElement>;
     10    /** Số tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */
     11    unread?: number;
    1012}
    1113
    12 export function ChatButton({ onClick, onIntent, config, buttonRef }: ChatButtonProps) {
     14export function ChatButton({ onClick, onIntent, config, buttonRef, unread = 0 }: ChatButtonProps) {
    1315    return (
    1416        <button
    … …  
    2022            onTouchStart={onIntent}
    2123            onFocus={onIntent}
    22             aria-label={t('openChat')}
     24            aria-label={unread > 0 ? `${t('openChat')} (${t('unreadMessages').replace('%d', String(unread))})` : t('openChat')}
    2325            aria-haspopup="dialog"
    2426        >
    … …  
    3032                </svg>
    3133            )}
     34            {unread > 0 && <span className="smb-unread" aria-hidden="true">{unread > 9 ? '9+' : unread}</span>}
    3235        </button>
    3336    );
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/components/ChatWindow.tsx

    r3761175 r3765552  
    66import { toPlainText } from '../markdown';
    77import { t } from '../i18n';
     8import { nextDelay, nextError, nextIdle, POLL } from '../pollSchedule';
    89import type { VoiceSession } from '../voice';
    910
    1011type VoiceModule = typeof import('../voice');
     12type CardsModule = typeof import('./ProductCards');
     13type CartPanelModule = typeof import('./CartPanel');
     14
     15/**
     16 * Thẻ sản phẩm (2.6.0) là chunk riêng: chỉ tải khi có tin nhắn kèm thẻ sản phẩm, thẻ đơn (S5) hoặc mã giảm giá
     17 * đề xuất (Starter).
     18 */
     19function hasCards(m: { metadata?: any }): boolean {
     20    const md = m.metadata;
     21    const some = (key: string) => Array.isArray(md[key]) && md[key].length > 0;
     22    return !!(md && (some('cards') || some('actions') || some('orders')));
     23}
    1124
    1225/** Nhập bằng giọng nói (2.5.0): chủ shop bật, trang HTTPS, trình duyệt có Web Speech API (Firefox chưa có). */
    … …  
    5063    order: 'stageOrder',
    5164    handoff: 'stageHandoff',
     65    queued: 'stageQueued',
    5266};
    5367
    … …  
    108122    const [listening, setListening] = useState(false);
    109123    const [voiceEnabled] = useState(() => voiceAvailable(config));
     124    const [cardsMod, setCardsMod] = useState<CardsModule | null>(null);
     125    // Giỏ trong chat (2.6.0, S4): chỉ khi có Store API; không có thì nút giỏ là link tới trang giỏ như trước.
     126    const store = { api: config.storeApi || '', cartUrl: config.cartUrl || '', checkoutUrl: config.checkoutUrl || '' };
     127    const [cartOpen, setCartOpen] = useState(false);
     128    const [cartPanelMod, setCartPanelMod] = useState<CartPanelModule | null>(null);
     129    const [cartCount, setCartCount] = useState<number | null>(null);
     130    const [cartRefresh, setCartRefresh] = useState(0);
    110131
    111132    const voiceModRef = useRef<VoiceModule | null>(null);
    … …  
    118139    const cancelClearRef = useRef<HTMLButtonElement>(null);
    119140    const lastMessageIdRef = useRef<number | null>(null);
    120     const pollIntervalRef = useRef<number>(5000);
     141    // Nhịp poll thích ứng (pollSchedule.ts): nhịp khi rảnh và nhịp lùi khi lỗi mạng.
     142    const idleDelayRef = useRef<number>(POLL.idleStart);
     143    const errorDelayRef = useRef<number>(0);
     144    const stageRef = useRef<string | null>(null);
     145    const statusRef = useRef<string>(initialStatus || 'bot_active');
    121146    const timeoutRef = useRef<number | null>(null);
     147    /** Lúc gửi tin đang chờ câu trả lời (0 = không chờ). */
     148    const awaitingSinceRef = useRef<number>(0);
     149    /** Ngôn ngữ khách đang viết, máy chủ nhận từ tin nhắn (2.6.0); null = chưa biết. */
     150    const languageRef = useRef<string | null>(null);
    122151
    123152    const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
    … …  
    128157
    129158    useEffect(() => { inputRef.current?.focus(); }, []);
     159
     160    useEffect(() => {
     161        if (!cardsMod && messages.some(hasCards)) {
     162            import('./ProductCards').then(mod => setCardsMod(() => mod)).catch(() => { /* mạng lỗi: vẫn còn chữ */ });
     163        }
     164    }, [messages]);
    130165
    131166    // Tải sẵn module giọng nói khi khung chat mở (chỉ khi tính năng bật và trình duyệt hỗ trợ), để lúc khách
    … …  
    181216
    182217    const applyState = (data: any) => {
    183         if (typeof data.status === 'string') setStatus(data.status);
     218        if (typeof data.status === 'string') { setStatus(data.status); statusRef.current = data.status; }
    184219        if ('agent_name' in data) setAgentName(data.agent_name || null);
    185         if ('stage' in data) setStage(data.stage || null);
     220        if ('stage' in data) { setStage(data.stage || null); stageRef.current = data.stage || null; }
     221        if (typeof data.language === 'string' && data.language) languageRef.current = data.language;
    186222    };
    187223
    188224    const poll = async () => {
    189225        if (document.visibilityState !== 'visible') {
    190             timeoutRef.current = window.setTimeout(poll, pollIntervalRef.current);
     226            // Tab ẩn: không gọi máy chủ; lượt poll chạy lại ngay khi tab hiện (visibilitychange).
     227            timeoutRef.current = window.setTimeout(poll, POLL.idleMax);
    191228            return;
    192229        }
     230        let gotMessages = false;
    193231        try {
    194232            const data = await api.pollMessages(conversationId, lastMessageIdRef.current);
    … …  
    201239                const incoming = data.messages.filter((m: any) => m.sender_type !== 'user');
    202240                if (incoming.length > 0) {
     241                    awaitingSinceRef.current = 0;
    203242                    setIsTyping(false);
    204243                    setStage(null);
    … …  
    206245                    setLiveText(toPlainText(incoming[incoming.length - 1].content).slice(0, 300));
    207246                }
    208                 pollIntervalRef.current = 5000;
     247                gotMessages = true;
    209248            }
     249            errorDelayRef.current = 0;
     250            idleDelayRef.current = nextIdle(idleDelayRef.current, gotMessages);
    210251        } catch (err) {
    211             pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000);
    212         }
    213         timeoutRef.current = window.setTimeout(poll, pollIntervalRef.current);
    214     };
     252            errorDelayRef.current = nextError(errorDelayRef.current);
     253        }
     254        timeoutRef.current = window.setTimeout(poll, nextPollDelay());
     255    };
     256
     257    const nextPollDelay = () => nextDelay({
     258        now: Date.now(),
     259        awaitingSince: awaitingSinceRef.current,
     260        stage: stageRef.current,
     261        status: statusRef.current,
     262        idleDelay: idleDelayRef.current,
     263        errorDelay: errorDelayRef.current,
     264    });
    215265
    216266    useEffect(() => {
    … …  
    219269        const handleVisibility = () => {
    220270            if (document.visibilityState === 'visible') {
    221                 pollIntervalRef.current = 5000;
     271                idleDelayRef.current = POLL.idleStart;
     272                errorDelayRef.current = 0;
    222273                if (timeoutRef.current) clearTimeout(timeoutRef.current);
    223274                poll();
    … …  
    232283
    233284    const pollSoon = () => {
    234         // Việc gọi AI được xếp hàng đợi và chính request poll là nơi chạy nó, nên poll ngay.
     285        // Câu về giỏ hàng do chính lượt poll của khách trả lời (cần cookie phiên), nên poll ngay.
    235286        if (timeoutRef.current) clearTimeout(timeoutRef.current);
    236287        timeoutRef.current = window.setTimeout(poll, 100);
    … …  
    244295    };
    245296
    246     // Ngôn ngữ nhận dạng: <html lang> của trang (P4 sẽ thay bằng ngôn ngữ hội thoại), rồi ngôn ngữ trình duyệt.
     297    // Ngôn ngữ nhận dạng: ngôn ngữ khách đang viết trong hội thoại, rồi <html lang> của trang, rồi trình duyệt.
    247298    const voiceLang = (mod: VoiceModule) => mod.pickLanguage(
    248         document.documentElement.lang,
     299        languageRef.current || document.documentElement.lang,
    249300        navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language]
    250301    );
    … …  
    300351            const savedMsg = await api.sendMessage(conversationId, content, crypto.randomUUID());
    301352            setMessages(prev => prev.map(m => m.id === tempMsg.id ? savedMsg : m));
     353            if (status === 'bot_active') awaitingSinceRef.current = Date.now();
    302354            pollSoon();
    303355        } catch (err) {
    … …  
    372424
    373425    // Bấm link trong chat: tương tác đủ điều kiện cho chỉ số đơn có AI hỗ trợ.
     426    const toggleCart = () => {
     427        if (cartOpen) { setCartOpen(false); return; }
     428        import('./CartPanel').then(mod => { setCartPanelMod(() => mod); setCartOpen(true); }).catch(() => {
     429            // Không tải được chunk: mở trang giỏ của WooCommerce như trước.
     430            if (store.cartUrl) window.location.href = store.cartUrl;
     431        });
     432    };
     433
     434    const onCartAdd = (card: { id: number }, itemsCount: number, quantity = 1) => {
     435        setCartCount(itemsCount);
     436        setCartRefresh(n => n + 1);
     437        api.event(conversationId, 'cart_add', String(card.id), { qty: quantity });
     438    };
     439
    374440    const trackLink = (url: string) => {
    375441        try {
    … …  
    445511                </div>
    446512                <div className="smb-chat-actions">
    447                     {config.cartUrl && (
     513                    {config.cartUrl && (store.api ? (
     514                        <button type="button" className="smb-icon-btn smb-cart-btn" onClick={toggleCart} aria-expanded={cartOpen}
     515                            aria-controls="smb-cart-panel" aria-label={t('cart')} title={t('cart')}>
     516                            <Icon d={ICONS.cart} />
     517                            {cartCount ? <span className="smb-cart-badge" aria-hidden="true">{cartCount > 99 ? '99+' : cartCount}</span> : null}
     518                        </button>
     519                    ) : (
    448520                        <a className="smb-icon-btn" href={config.cartUrl} aria-label={t('cart')} title={t('cart')}><Icon d={ICONS.cart} /></a>
    449                     )}
     521                    ))}
    450522                    <button type="button" className="smb-icon-btn smb-expand-btn" onClick={() => setExpanded(!expanded)}
    451523                        aria-pressed={expanded} aria-label={expanded ? t('collapse') : t('expand')} title={expanded ? t('collapse') : t('expand')}>
    … …  
    471543
    472544            <div className="smb-chat-body">
     545                {cartOpen && cartPanelMod && (
     546                    <cartPanelMod.CartPanel store={store} refresh={cartRefresh} onClose={() => setCartOpen(false)} onCount={setCartCount}
     547                        proVersion={config.proModule || ''} />
     548                )}
    473549                {config.aiNotice && <p className="smb-ai-notice">{config.aiNotice}</p>}
    474550
    … …  
    484560                )}
    485561
    486                 {messages.map(m => (
     562                {messages.map(m => [
    487563                    <div key={m.id} className={`smb-msg ${m.sender_type === 'user' ? 'is-user' : m.sender_type === 'agent' ? 'is-agent' : 'is-bot'}`}>
    488564                        <div className="smb-bubble"><MessageContent text={m.content} rich={m.sender_type !== 'user'} onLinkClick={trackLink} /></div>
    489                     </div>
    490                 ))}
     565                    </div>,
     566                    cardsMod && m.sender_type !== 'user' && hasCards(m) ? (
     567                        <div key={`${m.id}-cards`} className="smb-msg is-bot smb-msg-cards">
     568                            <cardsMod.ProductCards metadata={m.metadata} onView={trackLink} store={store} onCartAdd={onCartAdd}
     569                                proVersion={config.proModule || ''} onCouponApplied={() => setCartRefresh(n => n + 1)} />
     570                            <cardsMod.OrderCards metadata={m.metadata} onView={trackLink} />
     571                        </div>
     572                    ) : null,
     573                ])}
    491574
    492575                {isTyping && (
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/i18n.ts

    r3761175 r3765552  
    1111const FALLBACK: Strings = {
    1212    openChat: 'Open chat',
     13    unreadMessages: 'new replies: %d',
    1314    chatWindow: 'Chat with our assistant',
    1415    cart: 'Cart',
    … …  
    5051    stageOrder: 'Looking up your order…',
    5152    stageHandoff: 'Connecting you with a team member…',
     53    stageQueued: 'Waiting in line…',
     54    productsLabel: 'Products',
     55    cardsPrev: 'Previous products',
     56    cardsNext: 'Next products',
     57    cardView: 'View',
     58    stockIn: 'In stock',
     59    stockOut: 'Out of stock',
     60    stockBackorder: 'On backorder',
     61    priceAt: 'Price on %s',
     62    addToCart: 'Add to cart',
     63    adding: 'Adding…',
     64    chooseOptions: 'Choose options',
     65    addedToCart: 'Added to cart',
     66    viewCart: 'View cart',
     67    checkout: 'Checkout',
     68    cartOutOfStock: 'Sorry, this item just sold out.',
     69    cartLimit: "You can't add more of this item.",
     70    cartError: "Couldn't add to cart.",
     71    openProduct: 'Open the product page',
     72    yourCart: 'Your cart',
     73    closeCart: 'Close',
     74    loadingCart: 'Loading your cart…',
     75    cartLoadError: "Couldn't load your cart.",
     76    cartEmpty: 'Your cart is empty.',
     77    subtotal: 'Subtotal',
     78    discount: 'Discount',
     79    ordersLabel: 'Your orders',
     80    orderTitle: 'Order #%s',
     81    orderMore: 'and %d more',
     82    orderTracking: 'Tracking number',
     83    orderTrack: 'Track',
     84    orderView: 'View order',
    5285    resolutionQuestion: 'Did this answer your question?',
    5386    yes: 'Yes',
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/index.tsx

    r3761175 r3765552  
    44import { api } from './api';
    55import { onColor } from './color';
     6import { PEEK_MS, countUnread, lastId, mergeMessages, readSeen, shouldPeek, writeSeen } from './peek';
    67import './style.css';
    78
    … …  
    4142    const wasOpen = useRef(false);
    4243    const [chatModule, setChatModule] = useState<ChatWindowModule | null>(null);
     44    // Tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9): peek.ts quyết định khi nào hỏi máy chủ.
     45    const [unread, setUnread] = useState(0);
     46    const seenRef = useRef({ id: 0, at: 0 });
     47    const live = useRef({ isOpen, conversationId, status, messages });
     48    live.current = { isOpen, conversationId, status, messages };
     49
     50    useEffect(() => {
     51        if (conversationId === null) return;
     52        const seen = readSeen(conversationId);
     53        // Lần đầu trên trình duyệt này: coi mọi tin đã có là đã thấy.
     54        seenRef.current = seen.id > 0 ? seen : { id: lastId(messages), at: Date.now() };
     55        if (seen.id === 0) writeSeen(conversationId, seenRef.current.id, seenRef.current.at);
     56        setUnread(countUnread(messages, seenRef.current.id));
     57    }, [conversationId]);
     58
     59    useEffect(() => {
     60        if (conversationId === null) return;
     61        // Đang mở (mọi tin hiện ra là đã thấy) hoặc vừa thu nhỏ (hội thoại vừa có hoạt động).
     62        // Ghi cả lúc đang mở, vì khách có thể đóng tab mà không thu nhỏ khung chat.
     63        if (isOpen || wasOpen.current) {
     64            seenRef.current = { id: lastId(messages), at: Date.now() };
     65            writeSeen(conversationId, seenRef.current.id, seenRef.current.at);
     66            setUnread(0);
     67        }
     68    }, [isOpen, messages]);
     69
     70    useEffect(() => {
     71        const timer = window.setInterval(async () => {
     72            const s = live.current;
     73            if (!shouldPeek({ open: s.isOpen, conversationId: s.conversationId, status: s.status,
     74                lastActivityAt: seenRef.current.at, visible: document.visibilityState === 'visible', now: Date.now() })) return;
     75            try {
     76                const data = await api.pollMessages(s.conversationId as number, lastId(s.messages) || null);
     77                if (data.status) setStatus(data.status);
     78                const incoming = (data.messages || []).filter((m: any) => Number(m.id) > lastId(live.current.messages));
     79                if (!incoming.length) return;
     80                const merged = mergeMessages(live.current.messages, incoming);
     81                setMessages(merged);
     82                seenRef.current = { ...seenRef.current, at: Date.now() };
     83                writeSeen(s.conversationId as number, seenRef.current.id, seenRef.current.at);
     84                setUnread(countUnread(merged, seenRef.current.id));
     85            } catch { /* mạng lỗi: thử lại ở nhịp sau */ }
     86        }, PEEK_MS);
     87        return () => window.clearInterval(timer);
     88    }, []);
    4389
    4490    const prefetchChat = () => {
    … …  
    232278                />
    233279            ) : (
    234                 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} />
     280                <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} unread={unread} />
    235281            )}
    236282        </div>
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/style.css

    r3761175 r3765552  
    9191
    9292.smb-commerce-widget-container .smb-launcher:hover { transform: scale(1.06); }
    93 .smb-commerce-widget-container .smb-launcher.has-logo { overflow: hidden; }
     93/* Ảnh logo tự bo tròn (luật img bên dưới), nên nút không cắt tràn: huy hiệu tin chưa đọc nằm ngoài vòng tròn. */
     94.smb-commerce-widget-container .smb-launcher { position: relative; }
     95/* Tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */
     96.smb-commerce-widget-container .smb-unread {
     97  position: absolute;
     98  top: -4px;
     99  right: -4px;
     100  min-width: 22px;
     101  height: 22px;
     102  padding: 0 6px;
     103  box-sizing: border-box;
     104  border-radius: 11px;
     105  background: #d63638;
     106  color: #fff;
     107  border: 2px solid #fff;
     108  font-size: 12px;
     109  font-weight: 600;
     110  line-height: 18px;
     111  text-align: center;
     112}
    94113.smb-launcher img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
    95114
    … …  
    247266.is-bot .smb-bubble { background: var(--smb-bg-soft); color: var(--smb-text); border-bottom-left-radius: 6px; }
    248267.is-agent .smb-bubble { background: #e8efff; color: var(--smb-text); border-bottom-left-radius: 6px; }
     268
     269/* Thẻ sản phẩm (2.6.0, S3): 2–3 thẻ cuộn ngang dưới câu trả lời. */
     270.smb-msg.smb-msg-cards { max-width: 100%; width: 100%; margin-top: -2px; flex-direction: column; }
     271.smb-cards { position: relative; width: 100%; }
     272.smb-cards-track {
     273  display: flex; gap: 8px; margin: 0; padding: 2px 2px 6px; list-style: none;
     274  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch;
     275}
     276.smb-cards-track::-webkit-scrollbar { display: none; }
     277.smb-pcard {
     278  flex: 0 0 82%; max-width: 260px; scroll-snap-align: start;
     279  display: flex; gap: 10px; align-items: flex-start; padding: 10px;
     280  border: 1px solid rgba(15, 23, 42, .1); border-radius: 14px; background: var(--smb-bg); color: var(--smb-text);
     281}
     282.smb-cards.is-single .smb-pcard { flex-basis: 100%; max-width: 100%; }
     283.smb-pcard-img { flex: 0 0 64px; width: 64px; height: 64px; border-radius: 10px; object-fit: cover; background: var(--smb-bg-soft); }
     284.smb-pcard-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; font-size: 13px; line-height: 1.35; }
     285.smb-commerce-widget-container .smb-pcard-name {
     286  color: var(--smb-text); font-weight: 600; text-decoration: none;
     287  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
     288}
     289.smb-commerce-widget-container .smb-pcard-name:hover { text-decoration: underline; }
     290.smb-pcard-price { margin: 0; font-weight: 600; }
     291.smb-pcard-price del { margin-right: 6px; color: var(--smb-text-soft); font-weight: 400; }
     292.smb-pcard-stock { margin: 0; font-size: 12px; color: #15803d; }
     293.smb-pcard-stock.is-outofstock { color: #b42318; }
     294.smb-pcard-stock.is-onbackorder { color: #b45309; }
     295.smb-pcard-at { margin: 0; font-size: 11px; color: var(--smb-text-soft); }
     296.smb-pcard-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
     297/* Tiền tố container: thắng reset ".smb-commerce-widget-container a" (bỏ viền, nền, padding). */
     298.smb-commerce-widget-container .smb-pcard-btn {
     299  display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border-radius: 999px;
     300  border: 1px solid var(--smb-primary); color: var(--smb-primary); background: transparent;
     301  font-size: 13px; font-weight: 600; text-decoration: none; cursor: pointer;
     302}
     303.smb-commerce-widget-container .smb-pcard-btn:hover { background: var(--smb-primary); color: var(--smb-on-primary); }
     304.smb-commerce-widget-container .smb-cards-nav {
     305  position: absolute; top: 50%; transform: translateY(-50%); z-index: 1;
     306  width: 30px; height: 30px; border-radius: 50%; border: 0; cursor: pointer;
     307  background: var(--smb-bg); color: var(--smb-text); border: 1px solid rgba(15, 23, 42, .12);
     308  box-shadow: 0 2px 8px rgba(15, 23, 42, .18); font-size: 20px; font-weight: 600; line-height: 1;
     309}
     310.smb-commerce-widget-container .smb-cards-nav.is-prev { left: -4px; }
     311.smb-commerce-widget-container .smb-cards-nav.is-next { right: -4px; }
     312.smb-pcard-btn:focus-visible, .smb-cards-nav:focus-visible, .smb-pcard-name:focus-visible { outline: 2px solid var(--smb-focus); outline-offset: 2px; }
     313@media (hover: none) { .smb-commerce-widget-container .smb-cards-nav { display: none; } }
     314
     315/* Giỏ hàng trong chat (2.6.0, S4). */
     316.smb-commerce-widget-container .smb-pcard-btn.is-primary { background: var(--smb-primary); color: var(--smb-on-primary); }
     317.smb-commerce-widget-container .smb-pcard-btn.is-primary:hover { filter: brightness(1.08); }
     318.smb-commerce-widget-container .smb-pcard-btn[disabled] { opacity: .7; cursor: progress; }
     319.smb-commerce-widget-container .smb-pcard-link { color: var(--smb-primary); text-decoration: underline; font-size: 13px; }
     320.smb-pcard-added { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
     321.smb-pcard-added-text { color: #15803d; font-weight: 600; font-size: 13px; }
     322.smb-pcard-error { flex-basis: 100%; color: #b42318; font-size: 12px; }
     323.smb-commerce-widget-container .smb-cart-btn { position: relative; }
     324.smb-cart-badge {
     325  position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
     326  background: #fff; color: var(--smb-primary); font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
     327}
     328.smb-cart-panel {
     329  margin: 0 0 8px; padding: 12px 14px; border-radius: 16px; background: var(--smb-bg);
     330  border: 1px solid rgba(15, 23, 42, .12); box-shadow: 0 6px 18px rgba(15, 23, 42, .12);
     331}
     332.smb-cart-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
     333.smb-cart-head h3 { margin: 0; font-size: 15px; font-weight: 700; color: var(--smb-text); }
     334.smb-cart-head h3:focus { outline: none; }
     335.smb-cart-note { margin: 4px 0; font-size: 13px; color: var(--smb-text-soft); }
     336.smb-cart-items { list-style: none; margin: 0; padding: 0; }
     337.smb-cart-item { display: flex; gap: 8px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid rgba(15, 23, 42, .08); font-size: 13px; }
     338.smb-cart-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
     339.smb-cart-qty { color: var(--smb-text-soft); white-space: nowrap; }
     340.smb-cart-line { font-weight: 600; white-space: nowrap; }
     341.smb-cart-subtotal { display: flex; justify-content: space-between; margin: 8px 0; font-size: 14px; }
     342.smb-cart-options { display: block; font-size: 12px; color: var(--smb-text-soft); }
     343
     344/* Thẻ đơn hàng (2.6.0, S5). */
     345.smb-ocards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; width: 100%; }
     346.smb-ocard {
     347  padding: 10px 12px; border: 1px solid rgba(15, 23, 42, .1); border-radius: 14px;
     348  background: var(--smb-bg); color: var(--smb-text); font-size: 13px; line-height: 1.4;
     349}
     350.smb-ocard-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 8px; }
     351.smb-ocard-status { padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; background: var(--smb-bg-soft); color: var(--smb-text); }
     352.smb-ocard-status.is-processing, .smb-ocard-status.is-on-hold { background: #fef3c7; color: #92400e; }
     353.smb-ocard-status.is-completed { background: #dcfce7; color: #166534; }
     354.smb-ocard-status.is-cancelled, .smb-ocard-status.is-failed, .smb-ocard-status.is-refunded { background: #fee2e2; color: #991b1b; }
     355.smb-ocard-meta { margin: 4px 0 0; color: var(--smb-text-soft); font-size: 12px; }
     356.smb-commerce-widget-container .smb-ocard-items { list-style: none; margin: 6px 0 0; padding: 0; }
     357.smb-ocard-items li { overflow-wrap: anywhere; }
     358.smb-ocard-more { color: var(--smb-text-soft); }
     359.smb-ocard-track { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; margin: 8px 0 0; }
     360.smb-ocard-track code { font-size: 12px; overflow-wrap: anywhere; }
     361.smb-ocard .smb-pcard-link { display: inline-block; margin-top: 6px; }
     362.smb-cart-discount { display: flex; justify-content: space-between; margin: -4px 0 8px; font-size: 13px; color: #15803d; }
     363.smb-cart-actions { display: flex; gap: 8px; flex-wrap: wrap; }
    249364
    250365.smb-commerce-widget-container .message-link { color: #1d4ed8; text-decoration: underline; overflow-wrap: anywhere; }
  • ninhle-ai-sales-assistant/trunk/src/JS/widget/voice.ts

    r3761175 r3765552  
    2525    vi: 'vi-VN', en: 'en-US', th: 'th-TH', id: 'id-ID', ms: 'ms-MY', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN',
    2626    fr: 'fr-FR', de: 'de-DE', es: 'es-ES', pt: 'pt-BR', it: 'it-IT', ru: 'ru-RU', nl: 'nl-NL', pl: 'pl-PL',
    27     tr: 'tr-TR', hi: 'hi-IN', ar: 'ar-SA', fil: 'fil-PH', km: 'km-KH', lo: 'lo-LA',
     27    tr: 'tr-TR', hi: 'hi-IN', ar: 'ar-SA', fil: 'fil-PH', km: 'km-KH', lo: 'lo-LA', uk: 'uk-UA',
    2828};
    2929
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/AIProviderFactory.php

    r3747521 r3765552  
    1313
    1414    /** Khoá setting chứa API key tương ứng mỗi provider. */
    15     private static function key_setting($provider) {
     15    public static function key_setting($provider) {
    1616        $map = [
    1717            'openai' => 'smbcommerce_openai_key',
    … …  
    2020        ];
    2121        return $map[$provider] ?? '';
     22    }
     23
     24    /** Provider dùng được: có khoá API trong plugin, hoặc (WordPress) có nhà cung cấp trong Connectors. */
     25    public static function is_configured($provider) {
     26        if ($provider === Providers\WordPressAiProvider::NAME) {
     27            return Providers\WordPressAiProvider::available();
     28        }
     29        $key_setting = self::key_setting($provider);
     30        return $key_setting !== '' && !empty(SettingsManager::get_setting($key_setting));
    2231    }
    2332
    … …  
    3746                if (class_exists('\\Smartbuy\SMBCommerce\\AI\\Providers\\GeminiProvider')) {
    3847                    return new Providers\GeminiProvider();
     48                }
     49                break;
     50            // Kết nối AI của WordPress 7.0+ (2.6.0, S7): khoá nằm trong Settings → Connectors của WordPress.
     51            case Providers\WordPressAiProvider::NAME:
     52                if (Providers\WordPressAiProvider::available()) {
     53                    return new Providers\WordPressAiProvider();
    3954                }
    4055                break;
    … …  
    6580        $chain = [];
    6681        foreach ($order as $name) {
    67             $key_setting = self::key_setting($name);
    68             if ($key_setting === '' || empty(SettingsManager::get_setting($key_setting))) {
     82            if (!self::is_configured($name)) {
    6983                continue;
    7084            }
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/AIService.php

    r3752682 r3765552  
    8787            $user_context .= "No user info provided yet. Do not assume their name.\n";
    8888        }
    89         $user_context .= "- Order history: unknown. Never invent orders. To look one up, call the get_order_status tool after the customer provides an order code or email.\n";
     89        if (\Smartbuy\SMBCommerce\Support\CustomerContext::user_id() > 0 && class_exists('WooCommerce')) {
     90            // S5: khách đăng nhập lúc gửi tin; tài khoản là bằng chứng sở hữu đơn.
     91            $user_context .= "- The customer is logged in to their store account. For questions about their orders or deliveries, call get_my_orders first; do not ask for an order number, email or phone. Never invent orders.\n";
     92        } else {
     93            $user_context .= "- Order history: unknown. Never invent orders. To look one up, call the get_order_status tool after the customer provides an order code or email.\n";
     94        }
    9095
    9196        // Tin nhắn mới nhất của khách: dùng làm từ khoá truy vấn catalog và trích nội dung link/file.
    … …  
    100105        // WooCommerce Product RAG Context
    101106        $product_context = '';
     107        $context_ids = [];
    102108        if (class_exists('\\Smartbuy\SMBCommerce\\WooCommerce\\WooCommerceProductScanner')) {
    103109            $scanner = new \Smartbuy\SMBCommerce\WooCommerce\WooCommerceProductScanner();
    … …  
    105111                $latest_user_message !== '' ? [$latest_user_message] : []
    106112            );
     113            $context_ids = $scanner->context_ids();
    107114        }
    108115
    … …  
    123130        $sales_playbook = SalesPlaybook::build($playbook_state);
    124131
    125         $capability_limits = self::capability_limits();
     132        // Starter (S4): đặt sẵn sản phẩm khách đã chọn, mã giảm giá chủ shop cho phép; '' ở bản Free.
     133        $woo = class_exists('WooCommerce');
     134        $cart_actions_prompt = $woo ? \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::prompt_block() : '';
     135        $capability_limits = self::capability_limits($woo && \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::offers_coupons());
    126136
    127137        // Kịch bản đứng đầu để định hình vai trò, sau đó mới tới ràng buộc và dữ liệu.
    … …  
    130140            $guardrails,
    131141            $capability_limits,
     142            $cart_actions_prompt,
    132143            $master_prompt,
    133144            $handoff_instructions,
    134145            $suggestion_instructions,
     146            // Thẻ sản phẩm (2.6.0, S3): chỉ có nghĩa khi có cửa hàng WooCommerce.
     147            class_exists('WooCommerce') ? \Smartbuy\SMBCommerce\Commerce\CardBuilder::PROMPT : '',
     148            // Sales Insights (2.6.0, S6): model đánh dấu câu nó không trả lời được từ dữ liệu của shop.
     149            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::PROMPT,
    135150            $user_context,
    136151            $history_digest,
    … …  
    152167                $final_system_prompt .= "\n\n[SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES]\n" . $extracted_content;
    153168            }
     169        }
     170
     171        // Ngôn ngữ trả lời (S2): khối đặt CUỐI, sau phần cố định để prompt cache của nhà cung cấp vẫn
     172        // trúng; nằm trong khoá cache câu trả lời bên dưới nên mỗi ngôn ngữ có câu trả lời riêng.
     173        $language_block = ReplyLanguage::prompt_block($conversation['language'] ?? null);
     174        if ($language_block !== '') {
     175            $final_system_prompt .= "\n\n" . $language_block;
    154176        }
    155177
    … …  
    187209
    188210        if (\Smartbuy\SMBCommerce\Commerce\LocalResolver::is_local($routing)) {
     211            // Câu trả lời tại chỗ về đơn hàng cũng có thẻ đơn (S5): LocalHandler ghi đơn đã kiểm vào sổ.
     212            ToolResultLedger::reset();
    189213            $local_reply = \Smartbuy\SMBCommerce\Commerce\LocalHandler::handle($routing);
    190214
    191215            if ($local_reply !== null && $local_reply !== '') {
    192                 MessageRepository::add_message($conversation_id, 'system', $local_reply);
     216                $local_orders = \Smartbuy\SMBCommerce\Commerce\CardBuilder::order_cards($local_reply);
     217                \Smartbuy\SMBCommerce\Commerce\CustomerProof::from_ledger($conversation_id);
     218                MessageRepository::add_message($conversation_id, 'system', $local_reply, null, $local_orders ? ['orders' => $local_orders] : null);
    193219                self::log_ai_request($conversation_id, 'chat', 0, 0, 0, 'local', false, $routing['reason']);
    194220                return true;
    … …  
    237263                \Smartbuy\SMBCommerce\Commerce\RoutingReason::UNRESOLVED_LOCAL_ONLY
    238264            );
     265            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'local_only');
    239266            return true;
    240267        }
    … …  
    318345        $rounds = 0;
    319346        $tool_handoff_priority = null;
     347        // Model chuyển nhân viên vì thiếu thông tin (create_handoff cause=missing_information): câu chưa trả lời được.
     348        $handoff_missing_info = false;
    320349        // Gom kết quả tool để đối chiếu số liệu trong câu trả lời cuối.
    321350        $grounding_from_tools = '';
    … …  
    329358            'contact_id' => (int) ($conversation['contact_id'] ?? 0),
    330359        ]);
     360        // Sản phẩm tool trả về trong lượt này, cùng sản phẩm thật trong [PRODUCT CATALOG] của lượt: chỉ chúng mới
     361        // được hiện thành thẻ hoặc đề xuất vào giỏ.
     362        ToolResultLedger::reset();
     363        ToolResultLedger::know($context_ids);
    331364
    332365        while (!empty($tool_calls) && $rounds < self::MAX_TOOL_ROUNDS) {
    … …  
    341374                self::set_stage($conversation_id, self::STAGE_BY_TOOL[$tool_name] ?? 'thinking');
    342375                $result = ToolExecutor::execute_tool($tool_name, $tool_params);
     376                ToolResultLedger::record($tool_name, $result);
     377                \Smartbuy\SMBCommerce\Insights\InsightsRecorder::tool_result($conversation_id, $tool_name, $tool_params, $result);
    343378
    344379                // Gọi tool handoff nghĩa là thật sự phải chuyển cho người thật,
    … …  
    346381                if ($tool_name === 'create_handoff' && !empty($result['success'])) {
    347382                    $tool_handoff_priority = $tool_params['priority'] ?? 'normal';
     383                    $handoff_missing_info = ($tool_params['cause'] ?? '') === 'missing_information';
    348384                }
    349385
    … …  
    369405            // Provider tự dựng message theo định dạng của mình.
    370406            $provider->append_tool_turn($messages_context, $assistant_message, $tool_results);
     407
     408            // Lượt chạy nền gần hết ngân sách (TurnClock): đừng mở thêm lời gọi chắc chắn bị cắt giữa chừng.
     409            $left = TurnClock::remaining();
     410            if ($left !== null && $left < 15) {
     411                \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant: tool round ' . $rounds . ' skipped, the background turn is out of time.');
     412                break;
     413            }
    371414
    372415            $call_start = microtime(true);
    … …  
    396439        $reply_content = self::clean_reply($raw_reply);
    397440
     441        // Thẻ sản phẩm (S3): đọc và bỏ dấu [[show:…]] TRƯỚC khối gợi ý, vì khối gợi ý thiếu thẻ đóng
     442        // sẽ nuốt mọi thứ phía sau nó.
     443        $card_result = \Smartbuy\SMBCommerce\Commerce\CardBuilder::from_reply($reply_content);
     444        $reply_content = $card_result['text'];
     445        // Sales Insights (S6): dấu [[unknown]] = câu này không trả lời được từ dữ liệu của shop.
     446        $unknown = \Smartbuy\SMBCommerce\Insights\InsightsRecorder::strip_unknown($reply_content);
     447        $reply_content = $unknown['text'];
     448        // Starter (S4): thẻ đề xuất kèm lựa chọn sẵn, và mã giảm giá có nút áp.
     449        $cards = ToolResultLedger::merge_action_cards($card_result['cards']);
     450        $cart_actions = ToolResultLedger::actions();
     451
    398452        // Hết vòng mà model vẫn đòi gọi tool: đừng trả về câu rỗng cho khách.
    399453        if (!empty($tool_calls) && trim($reply_content) === '') {
    400             $reply_content = __('I am still looking this up for you, one moment please.', 'ninhle-ai-sales-assistant');
     454            // Không còn gì chạy tiếp sau câu này: đừng hứa "một lát nữa" (câu cũ làm khách chờ mãi, staging 25/10).
     455            $reply_content = __('Sorry, I could not finish looking that up. Please ask me again in a moment.', 'ninhle-ai-sales-assistant');
    401456        }
    402457
    … …  
    450505            }
    451506        }
     507        if (!empty($cards)) {
     508            $metadata = (array) $metadata + ['cards' => $cards];
     509        }
     510        if (!empty($cart_actions)) {
     511            $metadata = (array) $metadata + ['actions' => $cart_actions];
     512        }
     513        // Thẻ đơn (S5): đơn đã qua kiểm sở hữu trong lượt, chọn theo câu trả lời.
     514        $order_cards = \Smartbuy\SMBCommerce\Commerce\CardBuilder::order_cards($reply_content);
     515        \Smartbuy\SMBCommerce\Commerce\CustomerProof::from_ledger($conversation_id);
     516        if (!empty($order_cards)) {
     517            $metadata = (array) $metadata + ['orders' => $order_cards];
     518        }
    452519
    453520        // --- Đối chiếu mọi con số tiền với dữ liệu thật đã đưa cho model ---
    … …  
    474541        $saved_reply = MessageRepository::add_message($conversation_id, 'system', $reply_content, null, $metadata);
    475542        self::record_reply_events($conversation_id, $saved_reply, $products_shown, $product_lookups);
     543        // Sales Insights (S6): một tin của khách chỉ tính một lần; lý do đầu tiên khớp được ghi.
     544        if ($unknown['unknown']) {
     545            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'unknown');
     546        } elseif ($product_lookups > 0 && $products_shown === 0) {
     547            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'no_data');
     548        } elseif ($handoff_missing_info) {
     549            \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'handoff');
     550        }
    476551
    477552        // Chỉ cache khi lượt này không gọi tool nào — kết quả tool là dữ liệu sống.
    … …  
    535610        'get_product_variations' => 'products',
    536611        'get_order_status' => 'order',
     612        'get_my_orders' => 'order',
    537613        'get_customer_downloads' => 'order',
    538614        'create_handoff' => 'handoff',
    … …  
    679755     * HỢP LỆ, bot trả lời "mã không hợp lệ". Trả về '' khi gói đã có đủ công cụ.
    680756     */
    681     private static function capability_limits() {
     757    private static function capability_limits($has_coupon_offers = false) {
    682758        $gate = '\\Smartbuy\\SMBCommerce\\Licensing\\LicenseGate';
    683759        $limits = [];
    684760        if (!$gate::can('coupon_tools')) {
    685             $limits[] = '- You cannot check coupon or discount codes. Never say a code is valid, invalid, expired or how much it saves. Tell the customer to enter the code in the cart or at checkout, where the store applies it automatically.';
     761            $limits[] = '- You cannot check coupon or discount codes. Never say a code is valid, invalid, expired or how much it saves. Tell the customer to enter the code in the cart or at checkout, where the store applies it automatically.'
     762                . ($has_coupon_offers ? ' The only exception is the store-approved codes under [COUPONS YOU MAY OFFER].' : '');
    686763        }
    687764        if (!$gate::can('product_variations')) {
    … …  
    726803     *                    và retry_count; rỗng với lượt không gọi AI (local, cache, local_only).
    727804     */
    728     private static function log_ai_request($conversation_id, $request_type, $prompt_tokens, $completion_tokens, $latency_ms, $provider_name, $error_flag, $decision, array $call = []) {
     805    public static function log_ai_request($conversation_id, $request_type, $prompt_tokens, $completion_tokens, $latency_ms, $provider_name, $error_flag, $decision, array $call = []) {
    729806        global $wpdb;
    730807        $table = $wpdb->prefix . SMBCOMMERCE_PREFIX . 'ai_logs';
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/ClaudeProvider.php

    r3752682 r3765552  
    172172    }
    173173
     174    /**
     175     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     176     *
     177     * @param array $options model (thay model chat), max_tokens, json (chỉ là gợi ý: Anthropic theo prompt).
     178     * @return array{text: string, usage: array}|\WP_Error
     179     */
     180    public function complete($system, $user, array $options = []) {
     181        if (empty($this->api_key)) {
     182            return new \WP_Error('missing_key', 'Anthropic API Key is missing');
     183        }
     184        $model = (string) ($options['model'] ?? $this->model);
     185        $response = $this->call_api([
     186            'model' => $model,
     187            'max_tokens' => (int) ($options['max_tokens'] ?? 400),
     188            'system' => (string) $system,
     189            'messages' => [['role' => 'user', 'content' => (string) $user]],
     190        ]);
     191        if (is_wp_error($response)) {
     192            return $response;
     193        }
     194        $text = '';
     195        foreach ((array) ($response['content'] ?? []) as $block) {
     196            if (is_array($block) && ($block['type'] ?? '') === 'text') {
     197                $text .= (string) ($block['text'] ?? '');
     198            }
     199        }
     200        $cache_read = (int) ($response['usage']['cache_read_input_tokens'] ?? 0);
     201        $cache_write = (int) ($response['usage']['cache_creation_input_tokens'] ?? 0);
     202        return [
     203            'text' => $text,
     204            'usage' => [
     205                'prompt_tokens' => (int) ($response['usage']['input_tokens'] ?? 0) + $cache_read + $cache_write,
     206                'completion_tokens' => (int) ($response['usage']['output_tokens'] ?? 0),
     207                'cached_input_tokens' => $cache_read,
     208                'cache_write_tokens' => $cache_write,
     209                'model' => (string) ($response['model'] ?? $model),
     210                'reported' => isset($response['usage']),
     211            ],
     212        ];
     213    }
     214
    174215    public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) {
    175216        if (!empty($assistant_message)) {
    … …  
    199240            ],
    200241            'body'    => wp_json_encode($payload),
    201             'timeout' => 30,
     242            // 30 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     243            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(30) : 30,
    202244        ]);
    203245       
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/GeminiProvider.php

    r3752682 r3765552  
    1919    // ngừng lần lượt và khi đó mọi lượt chat đều lỗi 404 (đo trên staging 2026-10-09).
    2020    const DEFAULT_MODEL = 'gemini-flash-latest';
     21
     22    /** Token cộng thêm vào max_tokens của complete() cho phần suy nghĩ của model. */
     23    const THINKING_HEADROOM = 2048;
    2124
    2225    private $model = self::DEFAULT_MODEL;
    … …  
    116119       
    117120        $response = $this->call_api($payload);
    118        
     121
    119122        if (is_wp_error($response)) {
    120123            return $response;
    121124        }
    122        
    123         $reply = '';
    124         if (isset($response['candidates'][0]['content']['parts'][0]['text'])) {
    125             $reply = $response['candidates'][0]['content']['parts'][0]['text'];
     125
     126        $reply = self::candidate_text($response);
     127        // Lần gọi rỗng vẫn tính tiền token đầu vào: cộng vào usage nếu phải gọi lại.
     128        $spent = ['prompt' => 0, 'completion' => 0, 'cached' => 0];
     129
     130        // Candidate rỗng (không chữ, không lệnh tool): Gemini thỉnh thoảng trả như vậy và khách nhận câu "không
     131        // trả lời được" (staging 25/10). Ghi lý do để chẩn đoán, gọi lại MỘT lần nếu lượt còn thời gian.
     132        if (trim($reply) === '') {
     133            \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini returned no text (finishReason: '
     134                . (string) ($response['candidates'][0]['finishReason'] ?? ($response['promptFeedback']['blockReason'] ?? 'none')) . ').');
     135            $left = class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::remaining() : null;
     136            $blocked = in_array($response['candidates'][0]['finishReason'] ?? '', ['SAFETY', 'PROHIBITED_CONTENT', 'BLOCKLIST', 'SPII'], true)
     137                || isset($response['promptFeedback']['blockReason']);
     138            if (!$blocked && ($left === null || $left > 30)) {
     139                $first = $response['usageMetadata'] ?? [];
     140                $spent = [
     141                    'prompt' => (int) ($first['promptTokenCount'] ?? 0),
     142                    'completion' => (int) ($first['candidatesTokenCount'] ?? 0) + (int) ($first['thoughtsTokenCount'] ?? 0),
     143                    'cached' => (int) ($first['cachedContentTokenCount'] ?? 0),
     144                ];
     145                $retry = $this->call_api($payload);
     146                if (!is_wp_error($retry)) {
     147                    $response = $retry;
     148                    $reply = self::candidate_text($response);
     149                }
     150            }
    126151        }
    127152
    … …  
    136161        $meta = $response['usageMetadata'] ?? [];
    137162        $usage = [
    138             'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0),
    139             'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0),
    140             'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0),
     163            'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0) + $spent['prompt'],
     164            'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0) + $spent['completion'],
     165            'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0) + $spent['cached'],
    141166            'cache_write_tokens' => 0,
    142167            'model' => (string) ($response['modelVersion'] ?? $this->model),
    … …  
    149174            'usage' => $usage,
    150175            'assistant_message' => null, // Gemini dùng tool dạng text, không có khối riêng
     176        ];
     177    }
     178
     179    /**
     180     * Chữ của candidate đầu: ghép mọi phần có chữ, bỏ phần suy nghĩ (thought). Model đời mới có thể trả nhiều
     181     * phần, và phần đầu không nhất thiết là câu trả lời.
     182     */
     183    public static function candidate_text($response) {
     184        $text = '';
     185        foreach ((array) ($response['candidates'][0]['content']['parts'] ?? []) as $part) {
     186            if (is_array($part) && isset($part['text']) && empty($part['thought'])) {
     187                $text .= (string) $part['text'];
     188            }
     189        }
     190        return $text;
     191    }
     192
     193    /**
     194     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     195     *
     196     * @param array $options model (thay model chat), max_tokens, json (responseMimeType application/json).
     197     * @return array{text: string, usage: array}|\WP_Error
     198     */
     199    public function complete($system, $user, array $options = []) {
     200        if (empty($this->api_key)) {
     201            return new \WP_Error('missing_key', 'Gemini API Key is missing');
     202        }
     203        $chat_model = $this->model;
     204        $this->model = (string) ($options['model'] ?? $chat_model);
     205        // Model Gemini 2.5+/3.x "suy nghĩ" trước khi trả lời và token suy nghĩ tính vào maxOutputTokens: 300 token
     206        // cho một nhãn JSON từng ra câu trả lời rỗng (gemini-3.8-flash dùng ~220 token cho một câu ngắn, ninhle.com
     207        // 26/10). Chừa thêm chỗ; chi phí vẫn theo token thật đã dùng.
     208        $config = ['maxOutputTokens' => (int) ($options['max_tokens'] ?? 2048) + self::THINKING_HEADROOM];
     209        if (!empty($options['json'])) {
     210            $config['responseMimeType'] = 'application/json';
     211        }
     212        $response = $this->call_api([
     213            'systemInstruction' => ['parts' => [['text' => (string) $system]]],
     214            'contents' => [['role' => 'user', 'parts' => [['text' => (string) $user]]]],
     215            'generationConfig' => $config,
     216        ]);
     217        $model = $this->model;
     218        $this->model = $chat_model;
     219        if (is_wp_error($response)) {
     220            return $response;
     221        }
     222        $meta = $response['usageMetadata'] ?? [];
     223        return [
     224            'text' => self::candidate_text($response),
     225            'usage' => [
     226                'prompt_tokens' => (int) ($meta['promptTokenCount'] ?? 0),
     227                'completion_tokens' => (int) ($meta['candidatesTokenCount'] ?? 0) + (int) ($meta['thoughtsTokenCount'] ?? 0),
     228                'cached_input_tokens' => (int) ($meta['cachedContentTokenCount'] ?? 0),
     229                'cache_write_tokens' => 0,
     230                'model' => (string) ($response['modelVersion'] ?? $model),
     231                'reported' => isset($response['usageMetadata']),
     232            ],
    151233        ];
    152234    }
    … …  
    188270    }
    189271
    190     private function call_api($payload) {
     272    /**
     273     * @param bool $retried Đã thử lại bằng DEFAULT_MODEL trong lượt này chưa.
     274     */
     275    private function call_api($payload, $retried = false) {
    191276        $url = $this->build_url();
    192277
    … …  
    194279            'headers' => $this->build_headers(),
    195280            'body'    => wp_json_encode($payload),
    196             'timeout' => 30, // increased from 20s
     281            // 30 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     282            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(30) : 30,
    197283        ]);
    198284       
    … …  
    213299            \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini API Error (HTTP ' . $http_code . '): '
    214300                . Redact::for_log($err_msg . ' | Body: ' . substr($body, 0, 500), [$this->api_key]));
     301            // Model đã lưu không dùng được với key này (Google đóng dần model cũ với key mới: "no longer available
     302            // to new users", ninhle.com 25–26/10; hoặc gõ sai tên): thử lại một lần bằng bí danh luôn trỏ tới bản
     303            // Flash hiện hành, để khách vẫn có câu trả lời. Chủ shop vẫn cần sửa model trong Settings.
     304            if ((int) $http_code === 404 && !$retried && $this->model !== self::DEFAULT_MODEL) {
     305                \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant: Gemini model ' . $this->model
     306                    . ' is not available to this API key; retried with ' . self::DEFAULT_MODEL . '. Change the Gemini model in Settings.');
     307                $this->model = self::DEFAULT_MODEL;
     308                return $this->call_api($payload, true);
     309            }
    215310            return new \WP_Error('gemini_error', $err_msg . ' (HTTP ' . $http_code . ')');
    216311        }
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/OpenAIProvider.php

    r3752682 r3765552  
    182182    }
    183183
     184    /**
     185     * Một lần gọi không tool cho việc chạy nền (2.6.0, S6: gán nhãn Sales Insights).
     186     *
     187     * @param array $options model (thay model chat), max_tokens, json (response_format json_object).
     188     * @return array{text: string, usage: array}|\WP_Error
     189     */
     190    public function complete($system, $user, array $options = []) {
     191        if (empty($this->api_key)) {
     192            return new \WP_Error('missing_key', 'OpenAI API Key is missing');
     193        }
     194        $model = (string) ($options['model'] ?? $this->model);
     195        $payload = [
     196            'model' => $model,
     197            'messages' => [
     198                ['role' => 'system', 'content' => (string) $system],
     199                ['role' => 'user', 'content' => (string) $user],
     200            ],
     201            'max_completion_tokens' => (int) ($options['max_tokens'] ?? 400),
     202        ];
     203        if (!empty($options['json'])) {
     204            $payload['response_format'] = ['type' => 'json_object'];
     205        }
     206        $response = $this->call_api($payload);
     207        if (is_wp_error($response)) {
     208            return $response;
     209        }
     210        return [
     211            'text' => (string) ($response['choices'][0]['message']['content'] ?? ''),
     212            'usage' => [
     213                'prompt_tokens' => (int) ($response['usage']['prompt_tokens'] ?? 0),
     214                'completion_tokens' => (int) ($response['usage']['completion_tokens'] ?? 0),
     215                'cached_input_tokens' => (int) ($response['usage']['prompt_tokens_details']['cached_tokens'] ?? 0),
     216                'cache_write_tokens' => 0,
     217                'model' => (string) ($response['model'] ?? $model),
     218                'reported' => isset($response['usage']),
     219            ],
     220        ];
     221    }
     222
    184223    public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) {
    185224        if (!empty($assistant_message)) {
    … …  
    204243            ],
    205244            'body'    => wp_json_encode($payload),
    206             'timeout' => 15,
     245            // 15 s trong request của khách; lượt chạy nền chờ lâu hơn trong ngân sách của lượt (TurnClock).
     246            'timeout' => class_exists('\\Smartbuy\\SMBCommerce\\AI\\TurnClock') ? \Smartbuy\SMBCommerce\AI\TurnClock::http_timeout(15) : 15,
    207247        ]);
    208248       
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/ResponseSanitizer.php

    r3752682 r3765552  
    3838        'IMPORTANT RULES',
    3939        "SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES",
     40        // 2.6.0
     41        'REPLY LANGUAGE',
     42        'PRODUCT CARDS',
    4043    ];
    4144
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/SalesPlaybook.php

    r3752682 r3765552  
    263263            'hesitating'    => self::contains($text, ['suy nghĩ', 'cân nhắc', 'để xem', 'từ từ', 'chưa cần', 'tính sau']),
    264264            'comparing'     => self::contains($text, ['so sánh', 'khác gì', 'chỗ khác', 'bên kia', 'nên chọn']),
    265             'ready_to_buy'  => self::contains($text, ['mua', 'đặt hàng', 'chốt', 'lấy', 'order', 'thanh toán']),
     265            // Tiếng Anh (2.6.0): khách nói "I would like size M, 2 of them" mà bot vẫn hỏi "dùng để làm gì?" (staging 25/10).
     266            'ready_to_buy'  => self::contains($text, ['mua', 'đặt hàng', 'chốt', 'lấy', 'order', 'thanh toán',
     267                'buy', 'add to cart', "i'll take", 'i’ll take', 'i will take', "i'd like", 'i’d like', 'i would like', 'i want']),
    266268            'asks_policy'   => self::contains($text, ['bảo hành', 'đổi trả', 'ship', 'giao hàng', 'vận chuyển', 'hoàn tiền']),
    267269        ];
    … …  
    275277        }
    276278
    277         if (!empty($s['ready_to_buy'])) {
     279        if (!empty($s['ready_to_buy']) && self::cart_actions_enabled()) {
     280            // Starter (S4): giỏ trong chat; không hỏi thêm, không xin số điện thoại để "xác nhận đơn".
     281            $hints[] = 'Khách có tín hiệu muốn mua. KHÔNG hỏi thêm câu tìm hiểu nhu cầu: tra sản phẩm (và biến thể nếu có) rồi gọi propose_cart_action với đúng lựa chọn và số lượng khách nói, ngay trong lượt này. Thiếu lựa chọn nào thì chỉ hỏi đúng lựa chọn đó.';
     282        } elseif (!empty($s['ready_to_buy'])) {
    278283            $hints[] = 'Khách có tín hiệu muốn mua. Xác nhận sản phẩm và giá, đưa link đặt hàng, và xin số điện thoại để xác nhận đơn.';
    279284        } elseif (!empty($s['price_concern'])) {
    … …  
    311316    }
    312317
     318    /** Giỏ trong chat nâng cao (Starter, S4) đang mở; bản Free: lớp Null trả false. */
     319    private static function cart_actions_enabled() {
     320        return class_exists('WooCommerce') && class_exists('\\Smartbuy\\SMBCommerce\\Commerce\\Woo\\CartActionService')
     321            && \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::enabled();
     322    }
     323
    313324    private static function contains($haystack, $needles) {
    314325        foreach ($needles as $needle) {
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/ToolExecutor.php

    r3747521 r3765552  
    3838            'get_product_variations' => '/smb-commerce/v1/tools/product-variations',
    3939            'check_coupon'           => '/smb-commerce/v1/tools/check-coupon',
     40            'propose_cart_action'    => '/smb-commerce/v1/tools/propose-cart-action',
    4041            'get_order_status'       => '/smb-commerce/v1/tools/order-status',
     42            'get_my_orders'          => '/smb-commerce/v1/tools/my-orders',
    4143            'get_customer_downloads' => '/smb-commerce/v1/tools/customer-downloads',
    4244            'get_policy'             => '/smb-commerce/v1/tools/policy',
  • ninhle-ai-sales-assistant/trunk/src/PHP/AI/ToolsHelper.php

    r3747521 r3765552  
    1919            'get_product_variations' => 'product_variations',
    2020            'check_coupon' => 'coupon_tools',
     21            'propose_cart_action' => 'cart_actions_advanced',
    2122        ];
    2223        $premium = \Smartbuy\SMBCommerce\Core\PremiumFallbacks::is_premium_build();
    23         $tools = array_values(array_filter($tools, function ($tool) use ($paid_tools, $premium) {
     24        // Đơn của tôi (S5): chỉ khi khách của lượt đã đăng nhập; khách vãng lai dùng get_order_status kèm bằng chứng.
     25        $logged_in = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id() > 0;
     26        $tools = array_values(array_filter($tools, function ($tool) use ($paid_tools, $premium, $logged_in) {
     27            if ($tool['name'] === 'get_my_orders') {
     28                return $logged_in;
     29            }
    2430            if (!isset($paid_tools[$tool['name']])) {
    2531                return true;
    … …  
    110116            ],
    111117            [
     118                'name' => 'propose_cart_action',
     119                'description' => 'Offer the customer a one-tap cart action shown under your reply. type "add": a product (from a tool result in this conversation) with the chosen option and quantity preselected on its card; the customer still taps Add, nothing is added by you. For products with options pass the customer\'s choice in options, e.g. {"Size": "M"} (no need to look up the variation first). type "coupon": an Apply button for a store coupon; only works for codes the store allows the assistant to offer, so never invent codes. Call it as soon as the customer has named the product and option/quantity, or asks for a discount.',
     120                'input_schema' => [
     121                    'type' => 'object',
     122                    'properties' => [
     123                        'type' => [
     124                            'type' => 'string',
     125                            'enum' => ['add', 'coupon'],
     126                            'description' => 'add = product card with selection; coupon = Apply coupon button'
     127                        ],
     128                        'product_id' => [
     129                            'type' => 'integer',
     130                            'description' => 'type add: WooCommerce product ID from a tool result'
     131                        ],
     132                        'options' => [
     133                            'type' => 'object',
     134                            'description' => 'type add, products with options: the customer\'s choice as option name → value, e.g. {"Size": "M", "Color": "Blue"}'
     135                        ],
     136                        'variation_id' => [
     137                            'type' => 'integer',
     138                            'description' => 'type add: alternatively, the variation ID from get_product_variations'
     139                        ],
     140                        'quantity' => [
     141                            'type' => 'integer',
     142                            'description' => 'type add: quantity the customer asked for (default 1)'
     143                        ],
     144                        'code' => [
     145                            'type' => 'string',
     146                            'description' => 'type coupon: coupon code'
     147                        ]
     148                    ],
     149                    'required' => ['type']
     150                ],
     151                'endpoint' => $base_url . '/propose-cart-action'
     152            ],
     153            [
    112154                'name' => 'get_order_status',
    113155                'description' => 'Look up an order. Requires BOTH an order reference AND proof that the customer owns it — an order code alone is never enough, because order numbers are sequential and guessable. Ask the customer for the email address, phone number or order key used on the order before calling this.',
    … …  
    141183            ],
    142184            [
     185                'name' => 'get_my_orders',
     186                'description' => 'List the logged-in customer\'s 5 most recent orders with number, date, status, total, items and tracking. The store account is the proof of ownership, so do not ask for an order number, email or phone. The chat shows the order you talk about as a card; mention its order number.',
     187                'input_schema' => [
     188                    'type' => 'object',
     189                    'properties' => (object) [],
     190                    'required' => []
     191                ],
     192                'endpoint' => $base_url . '/my-orders'
     193            ],
     194            [
    143195                'name' => 'get_customer_downloads',
    144196                'description' => 'Get download links for digital products in ONE order. Requires the order reference AND proof of ownership. An email address alone is not proof — it appears on invoices and in leaked lists, and these links unlock paid goods.',
    … …  
    201253                            'enum' => ['low', 'normal', 'high', 'urgent'],
    202254                            'description' => 'Mức độ ưu tiên'
     255                        ],
     256                        // Sales Insights (2.6.0, S6): missing_information = câu shop nên thêm câu trả lời.
     257                        'cause' => [
     258                            'type' => 'string',
     259                            'enum' => ['customer_asked', 'complaint', 'order_problem', 'missing_information', 'other'],
     260                            'description' => 'Why a person is needed. Use missing_information when the store data does not contain the answer.'
    203261                        ],
    204262                        'email' => [
  • ninhle-ai-sales-assistant/trunk/src/PHP/Admin/AdminMenu.php

    r3752682 r3765552  
    7575            'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')),
    7676            'nonce' => wp_create_nonce('wp_rest'),
    77             // Chuỗi của tab "AI Value & Performance" dịch ở PHP để vào được file .pot.
    78             'i18n' => self::value_strings(),
     77            // Chuỗi của tab "AI Value & Performance" và "Sales Insights" dịch ở PHP để vào được file .pot.
     78            'i18n' => self::value_strings() + self::insights_strings(),
    7979        ];
    8080
    … …  
    8484            $config
    8585        );
     86    }
     87
     88    /** Chuỗi giao diện của tab "Sales Insights" (2.6.0, S6); các khoá chung (period, sample, save…) ở value_strings. */
     89    private static function insights_strings() {
     90        return [
     91            'insightsTab' => __('Sales Insights', 'ninhle-ai-sales-assistant'),
     92            'insightsIntro' => __('What shoppers asked that the assistant could not answer, what they looked for but could not get, and why they did not buy. Contact details are hidden.', 'ninhle-ai-sales-assistant'),
     93            /* translators: %s: number of chats */
     94            'chatsInPeriod' => __('%s chats in this period', 'ninhle-ai-sales-assistant'),
     95            /* translators: 1: number of chats so far, 2: number of chats needed */
     96            'notEnoughYet' => __('Not enough data yet (%1$s of %2$s chats); the lists below are a first look.', 'ninhle-ai-sales-assistant'),
     97
     98            'unansweredTitle' => __('Questions the assistant could not answer', 'ninhle-ai-sales-assistant'),
     99            'unansweredHelp' => __('Similar questions are grouped. Add an answer to teach the assistant, or dismiss questions you do not want to answer. A question shows again if shoppers keep asking it after you added an answer.', 'ninhle-ai-sales-assistant'),
     100            'unansweredEmpty' => __('No unanswered questions in this period.', 'ninhle-ai-sales-assistant'),
     101            'colQuestion' => __('Question', 'ninhle-ai-sales-assistant'),
     102            'colTimes' => __('Times asked', 'ninhle-ai-sales-assistant'),
     103            'colLast' => __('Last asked', 'ninhle-ai-sales-assistant'),
     104            'colActions' => __('Actions', 'ninhle-ai-sales-assistant'),
     105            /* translators: %s: other ways the question was asked */
     106            'alsoAsked' => __('Also asked as: %s', 'ninhle-ai-sales-assistant'),
     107            'reasonCustomerNo' => __('shopper said it was not answered', 'ninhle-ai-sales-assistant'),
     108            'reasonUnknown' => __('information missing', 'ninhle-ai-sales-assistant'),
     109            'reasonNoData' => __('no matching product', 'ninhle-ai-sales-assistant'),
     110            'reasonHandoff' => __('passed to your team', 'ninhle-ai-sales-assistant'),
     111            'reasonLocalOnly' => __('local-only mode', 'ninhle-ai-sales-assistant'),
     112            'addAnswer' => __('Add answer', 'ninhle-ai-sales-assistant'),
     113            'dismiss' => __('Dismiss', 'ninhle-ai-sales-assistant'),
     114            'dismissed' => __('Question dismissed.', 'ninhle-ai-sales-assistant'),
     115            'openChat' => __('Open a chat', 'ninhle-ai-sales-assistant'),
     116            'questionLabel' => __('Question', 'ninhle-ai-sales-assistant'),
     117            'answerLabel' => __('Answer', 'ninhle-ai-sales-assistant'),
     118            'targetLegend' => __('Where to save the answer', 'ninhle-ai-sales-assistant'),
     119            'targetBusiness' => __('Add to Business information (the assistant uses it from the next reply)', 'ninhle-ai-sales-assistant'),
     120            'targetNote' => __('Save as a note so I can update my policy pages', 'ninhle-ai-sales-assistant'),
     121            'saveAnswer' => __('Save answer', 'ninhle-ai-sales-assistant'),
     122            'cancel' => __('Cancel', 'ninhle-ai-sales-assistant'),
     123            'answerSavedBusiness' => __('Answer added to Business information.', 'ninhle-ai-sales-assistant'),
     124            'answerSavedNote' => __('Answer saved as a note.', 'ninhle-ai-sales-assistant'),
     125            'answerError' => __('Could not save the answer. Enter both the question and the answer.', 'ninhle-ai-sales-assistant'),
     126            'notesTitle' => __('Notes for your policy pages', 'ninhle-ai-sales-assistant'),
     127            'notesHelp' => __('Answers you chose to add to your own pages. The assistant does not read these notes.', 'ninhle-ai-sales-assistant'),
     128            'deleteNote' => __('Delete', 'ninhle-ai-sales-assistant'),
     129
     130            'unmetTitle' => __('What shoppers could not find', 'ninhle-ai-sales-assistant'),
     131            'noResultsTitle' => __('Searches with no matching product', 'ninhle-ai-sales-assistant'),
     132            'outOfStockTitle' => __('Wanted but out of stock', 'ninhle-ai-sales-assistant'),
     133            'noneYet' => __('None in this period.', 'ninhle-ai-sales-assistant'),
     134            'searchProducts' => __('Search your products', 'ninhle-ai-sales-assistant'),
     135            'backInStock' => __('In stock now', 'ninhle-ai-sales-assistant'),
     136            'backordersOn' => __('Backorders allowed', 'ninhle-ai-sales-assistant'),
     137            'restockHint' => __('Restock it, or allow backorders under Product data > Inventory.', 'ninhle-ai-sales-assistant'),
     138
     139            'whyTitle' => __('Why shoppers did not buy', 'ninhle-ai-sales-assistant'),
     140            'whyLocked' => __('Available in the Starter plan: each night an AI reads finished chats that did not lead to an order and sorts them by reason (price, shipping, size, stock, missing information and more), with example quotes.', 'ninhle-ai-sales-assistant'),
     141            'whyOff' => __('Turn on AI labeling below to see why shoppers did not buy.', 'ninhle-ai-sales-assistant'),
     142            'whyPaused' => __('AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on.', 'ninhle-ai-sales-assistant'),
     143            /* translators: 1: chats labeled so far, 2: chats needed */
     144            'notEnoughLabels' => __('Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough.', 'ninhle-ai-sales-assistant'),
     145            'whyPrice' => __('Price', 'ninhle-ai-sales-assistant'),
     146            'whyShipping' => __('Shipping cost or time', 'ninhle-ai-sales-assistant'),
     147            'whySizeFit' => __('Unsure about size or fit', 'ninhle-ai-sales-assistant'),
     148            'whyOutOfStock' => __('Out of stock', 'ninhle-ai-sales-assistant'),
     149            'whyMissingInfo' => __('Missing information', 'ninhle-ai-sales-assistant'),
     150            'whyWaitingDiscount' => __('Waiting for a discount', 'ninhle-ai-sales-assistant'),
     151            'whyJustBrowsing' => __('Just browsing', 'ninhle-ai-sales-assistant'),
     152            'whyOther' => __('Other', 'ninhle-ai-sales-assistant'),
     153            'seeUnanswered' => __('See the unanswered questions', 'ninhle-ai-sales-assistant'),
     154            /* translators: 1: AI provider and model, 2: amount spent this month, 3: monthly limit */
     155            'labelStatus' => __('Model: %1$s · This month: %2$s of %3$s', 'ninhle-ai-sales-assistant'),
     156            /* translators: %s: date */
     157            'lastRun' => __('Last run: %s', 'ninhle-ai-sales-assistant'),
     158            'stopBudget' => __('Stopped: the monthly limit for AI labeling was reached.', 'ninhle-ai-sales-assistant'),
     159            'stopGlobalBudget' => __('Stopped: the overall AI budget was reached.', 'ninhle-ai-sales-assistant'),
     160            'stopNoProvider' => __('Stopped: no AI provider with an API key is set up.', 'ninhle-ai-sales-assistant'),
     161            'stopNightly' => __('Paused: 200 chats are labeled per night at most.', 'ninhle-ai-sales-assistant'),
     162
     163            'aiSettingsTitle' => __('Sales Insights settings', 'ninhle-ai-sales-assistant'),
     164            'aiEnable' => __('Label why shoppers did not buy, each night with AI', 'ninhle-ai-sales-assistant'),
     165            'aiEnableHelp' => __('Runs at 2:00 site time on up to 200 finished chats that did not lead to an order, using your own AI provider key. Email addresses and phone numbers are hidden before anything is sent. Off by default.', 'ninhle-ai-sales-assistant'),
     166            'aiBudget' => __('Monthly limit (USD)', 'ninhle-ai-sales-assistant'),
     167            'aiModel' => __('Model', 'ninhle-ai-sales-assistant'),
     168            'aiModelCheapest' => __('Cheapest available (recommended)', 'ninhle-ai-sales-assistant'),
     169            'aiModelChat' => __('Same as the chat assistant', 'ninhle-ai-sales-assistant'),
     170            'digestEnable' => __('Email me a weekly summary', 'ninhle-ai-sales-assistant'),
     171            'digestHelp' => __('Every Monday morning to the site admin email: top unanswered questions, what shoppers could not find, the main reason they did not buy and the AI cost of the week. Not sent in a week without chats.', 'ninhle-ai-sales-assistant'),
     172        ];
    86173    }
    87174
  • ninhle-ai-sales-assistant/trunk/src/PHP/Analytics/OrderAttribution.php

    r3752682 r3765552  
    2424    const META_INTERACTION_AT = '_smbcommerce_interaction_at';
    2525
     26    /** Tương tác xảy ra khi AI chạy nền, chờ lượt poll kế tiếp của khách ghi vào phiên (2.6.0). */
     27    const PENDING_TRANSIENT = 'smbcommerce_attr_pending_';
     28
    2629    public static function init() {
    2730        add_action('woocommerce_checkout_order_processed', [self::class, 'on_classic_checkout'], 20, 3);
    … …  
    3235
    3336    /**
    34      * Nhớ tương tác đủ điều kiện trong phiên WooCommerce của khách. Chỉ chạy trong request của chính
    35      * khách (widget poll, bấm link); cron và WP-CLI không có phiên của ai nên trả false.
    36      *
     37     * Nhớ tương tác đủ điều kiện trong phiên WooCommerce của khách. Chỉ ghi phiên trong request của
     38     * chính khách (widget poll, bấm link). Ở nền (cron, WP-CLI, worker AI 2.6.0) không có phiên của ai:
     39     * tương tác được giữ lại để flush_deferred() ghi ở lượt poll kế tiếp, với đúng thời điểm gốc.
     40     *
     41     * @param string|null $at Thời điểm tương tác (UTC, Y-m-d H:i:s); null = bây giờ.
    3742     * @return bool true nếu đã lưu được vào phiên.
    3843     */
    39     public static function remember_interaction($conversation_id) {
     44    public static function remember_interaction($conversation_id, $at = null) {
    4045        $conversation_id = (int) $conversation_id;
    41         if ($conversation_id <= 0 || self::is_background_context() || !function_exists('WC')) {
     46        if ($conversation_id <= 0) {
     47            return false;
     48        }
     49        $at = is_string($at) && $at !== '' ? $at : gmdate('Y-m-d H:i:s');
     50
     51        if (self::is_background_context()) {
     52            if (function_exists('set_transient')) {
     53                set_transient(self::PENDING_TRANSIENT . $conversation_id, $at, 7 * 86400);
     54            }
     55            return false;
     56        }
     57
     58        if (!function_exists('WC')) {
    4259            return false;
    4360        }
    … …  
    6683        $wc->session->set(self::SESSION_KEY, [
    6784            'conversation_id' => $conversation_id,
    68             'at' => gmdate('Y-m-d H:i:s'),
     85            'at' => $at,
    6986        ]);
    7087        return true;
     88    }
     89
     90    /**
     91     * Ghi tương tác đang chờ (từ lượt AI chạy nền) vào phiên của khách. Gọi từ request poll của chính
     92     * khách; không có gì chờ thì chỉ tốn một lần đọc transient.
     93     *
     94     * @return bool true nếu đã ghi được vào phiên.
     95     */
     96    public static function flush_deferred($conversation_id) {
     97        $conversation_id = (int) $conversation_id;
     98        if ($conversation_id <= 0 || self::is_background_context() || !function_exists('get_transient')) {
     99            return false;
     100        }
     101        $key = self::PENDING_TRANSIENT . $conversation_id;
     102        $at = get_transient($key);
     103        if (!is_string($at) || $at === '') {
     104            return false;
     105        }
     106        delete_transient($key);
     107        return self::remember_interaction($conversation_id, $at);
    71108    }
    72109
    … …  
    220257
    221258    private static function is_background_context() {
    222         return (function_exists('wp_doing_cron') && wp_doing_cron()) || (defined('WP_CLI') && WP_CLI);
     259        return \Smartbuy\SMBCommerce\Support\RequestContext::is_background();
    223260    }
    224261}
  • ninhle-ai-sales-assistant/trunk/src/PHP/Analytics/ValueEvents.php

    r3752682 r3765552  
    2424    const ORDER_STATUS = 'order_status';
    2525    const ORDER_REFUNDED = 'order_refunded';
     26    /** Khách thêm sản phẩm vào giỏ ngay trong chat qua Store API (2.6.0, S4); meta {product_id, qty}. */
     27    const CART_ADD = 'cart_add';
     28    /** Câu khách hỏi mà bot không trả lời được (2.6.0, S6); meta {question_norm, reason}, câu đã che SĐT/email. */
     29    const UNANSWERED = 'unanswered';
     30    /** Khách tìm mà không có hàng, hoặc chỉ có hàng hết (S6); meta {terms, result: none|out_of_stock, product_ids}. */
     31    const UNMET_DEMAND = 'unmet_demand';
     32    /** Lý do không mua do gán nhãn đêm (S6, Starter); meta {reason, quote}, trích dẫn đã che. */
     33    const NO_PURCHASE_REASON = 'no_purchase_reason';
     34    /** Khách đã biết chắc (S12): đăng nhập, hoặc đã chứng minh sở hữu đơn; meta {user_id} hoặc {order_id}. */
     35    const CUSTOMER_VERIFIED = 'customer_verified';
    2636
    2737    /** Nguồn hợp lệ của cột source; giá trị lạ được ghi là 'system'. */
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/LocalHandler.php

    r3747521 r3765552  
    187187            'email' => $params['email'] ?? '',
    188188            'phone' => $params['phone'] ?? '',
     189            'user_id' => (int) ($params['user_id'] ?? 0),
    189190        ]);
    190191
    … …  
    210211        $order = $result->data();
    211212
     213        // Thẻ đơn (S5): đơn đã qua kiểm sở hữu ở trên, ghi như kết quả tool để AIService dựng thẻ.
     214        \Smartbuy\SMBCommerce\AI\ToolResultLedger::record('get_order_status', ['success' => true, 'order' => $order]);
     215
    212216        return sprintf(
    213217            /* translators: 1: order number. 2: order status. 3: date the order was placed. */
    214218            __('Order %1$s is currently: %2$s (placed on %3$s).', 'ninhle-ai-sales-assistant'),
    215219            $order['order_number'],
    216             $order['status'],
    217             $order['date']
     220            function_exists('wc_get_order_status_name') ? wc_get_order_status_name($order['status']) : $order['status'],
     221            // Ngày theo định dạng của site; present() trả giờ địa phương của shop dạng Y-m-d H:i:s.
     222            function_exists('date_i18n') && strtotime($order['date'])
     223                ? date_i18n((string) get_option('date_format', 'Y-m-d'), strtotime($order['date']))
     224                : $order['date']
    218225        );
    219226    }
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/LocalResolver.php

    r3747521 r3765552  
    7575
    7676    /**
     77     * Lệnh giỏ hàng/thanh toán là câu ngắn. Câu dài hơn thường hỏi về sản phẩm có chữ "thanh toán"/"cart"
     78     * ("Shop có plugin nào nhận thanh toán bằng mã QR cho WooCommerce không?", ninhle.com 25/10): để AI trả lời.
     79     */
     80    const MAX_COMMAND_LENGTH = 60;
     81
     82    /** Dấu hiệu câu hỏi: "thanh toán" trong câu hỏi là hỏi về cách trả tiền hay sản phẩm, không phải lệnh. */
     83    const QUESTION_MARKERS = [
     84        'không', 'nào', 'gì', 'bao nhiêu', 'thế nào', 'như nào', 'làm sao', 'được chưa', 'hỗ trợ',
     85        'how', 'what', 'which', 'do you', 'does', 'can i', 'is there', 'are there', 'accept',
     86    ];
     87
     88    /**
    7789     * Quyết định định tuyến cho một tin nhắn.
    7890     *
    … …  
    124136        }
    125137
    126         // --- Giỏ hàng và thanh toán ---
    127         if (self::contains_any($text, self::CHECKOUT_KEYWORDS)) {
     138        // --- Giỏ hàng và thanh toán: chỉ câu lệnh ngắn. "Thanh toán" trong câu hỏi thì để AI. ---
     139        $short = self::length($raw) <= self::MAX_COMMAND_LENGTH;
     140        $question = strpos($raw, '?') !== false || self::has_word_from($text, self::QUESTION_MARKERS);
     141        if ($short && !$question && self::contains_any($text, self::CHECKOUT_KEYWORDS)) {
    128142            return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'checkout');
    129143        }
    130144
    131         if (self::contains_any($text, self::CART_KEYWORDS)) {
     145        // Câu hỏi về giỏ của chính khách ("giỏ hàng của tôi có gì?") vẫn trả lời tại chỗ: AI chạy nền không đọc được giỏ.
     146        if ($short && self::contains_any($text, self::CART_KEYWORDS)) {
    132147            return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'view_cart');
    133148        }
    … …  
    178193    private static function length($text) {
    179194        return function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
     195    }
     196
     197    /** Câu (đã normalize) có chứa một trong các cụm, xét theo ranh giới từ ("nào" không khớp "nàoo"). */
     198    private static function has_word_from($text, array $phrases) {
     199        $padded = ' ' . $text . ' ';
     200        foreach ($phrases as $phrase) {
     201            if (strpos($padded, ' ' . self::normalize($phrase) . ' ') !== false) {
     202                return true;
     203            }
     204        }
     205        return false;
    180206    }
    181207
    … …  
    253279        }
    254280
     281        // Khách đã đăng nhập (S5): tài khoản là bằng chứng, OrderService chỉ khớp đơn của chính tài khoản đó.
     282        $customer = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id();
     283        if ($customer > 0) {
     284            $proof['user_id'] = $customer;
     285        }
     286
    255287        if (empty($proof)) {
    256288            return null;
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/CartService.php

    r3747521 r3765552  
    1818 * Vì sao điều đó nguy hiểm ở đây:
    1919 *
    20  *   Lời gọi tool của AI chạy trong MỘT TRONG HAI ngữ cảnh, và plugin không chọn
    21  *   được ngữ cảnh nào:
    22  *
    23  *     1. Request poll của widget (ChatController::get_messages -> run_one).
    24  *        Đây là request của chính khách, mang cookie của họ, nên WC()->cart
    25  *        đúng là giỏ của người đang chat.
    26  *
    27  *     2. Cron mỗi phút (JobDispatcher::CRON_HOOK), lo nốt hội thoại của khách
    28  *        đã đóng tab. Ở đây KHÔNG có cookie của ai cả.
    29  *
    30  *   Cùng một câu "thêm vào giỏ giúp em" sẽ rơi vào ngữ cảnh nào là tuỳ lúc
    31  *   khách còn mở tab hay không. Nếu cứ ghi bừa, kết quả là: lúc được lúc không,
    32  *   và tệ nhất là trên hosting có cache/session lạ thì ghi vào giỏ của NGƯỜI
    33  *   KHÁC — người vô tình kích hoạt cron.
     20 *   Lời gọi tool của AI chạy trong MỘT TRONG HAI loại ngữ cảnh, và plugin không
     21 *   chọn được ngữ cảnh nào:
     22 *
     23 *     1. Request poll của widget (ChatController::get_messages), với câu về giỏ
     24 *        hàng hoặc khi chạy nền bị chặn. Đây là request của chính khách, mang
     25 *        cookie của họ, nên WC()->cart đúng là giỏ của người đang chat.
     26 *
     27 *     2. Nền (2.6.0, AsyncKick): sau khi đã trả phản hồi, request loopback, hoặc
     28 *        cron mỗi phút cho khách đã đóng tab. Ở đây KHÔNG có cookie của ai cả,
     29 *        hoặc có nhưng không đặt được cookie mới.
     30 *
     31 *   Cùng một câu "thêm vào giỏ giúp em" sẽ rơi vào ngữ cảnh nào là tuỳ hosting
     32 *   và lúc khách còn mở tab hay không. Nếu cứ ghi bừa, kết quả là: lúc được lúc
     33 *   không, và tệ nhất là trên hosting có cache/session lạ thì ghi vào giỏ của
     34 *   NGƯỜI KHÁC — người vô tình kích hoạt cron.
    3435 *
    3536 * Nguyên tắc: chỉ động vào giỏ khi chắc chắn đang ở trong request của chính
    … …  
    252253
    253254    /**
    254      * Đang chạy ngoài request của khách hay không.
    255      *
    256      * Mỗi điều kiện ở đây tương ứng một đường thật sự tồn tại trong plugin:
    257      * cron chạy job tồn đọng, WP-CLI khi quản trị viên chạy lệnh, và giai đoạn
    258      * cài đặt/nâng cấp.
     255     * Đang chạy ngoài request của khách hay không: cron, WP-CLI, cài đặt/nâng cấp, hoặc worker nền
     256     * của plugin. Giữ tên cũ cho code gọi sẵn; định nghĩa nằm ở Support\RequestContext.
    259257     */
    260258    public static function is_background_context() {
    261         if (defined('DOING_CRON') && DOING_CRON) {
    262             return true;
    263         }
    264 
    265         if (function_exists('wp_doing_cron') && wp_doing_cron()) {
    266             return true;
    267         }
    268 
    269         if (defined('WP_CLI') && WP_CLI) {
    270             return true;
    271         }
    272 
    273         if (defined('WP_INSTALLING') && WP_INSTALLING) {
    274             return true;
    275         }
    276 
    277         return false;
     259        return \Smartbuy\SMBCommerce\Support\RequestContext::is_background();
    278260    }
    279261
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/OrderService.php

    r3747521 r3765552  
    3333    const PUBLIC_FIELDS = [
    3434        'id', 'order_number', 'status', 'date', 'total', 'currency', 'items',
    35         'payment_method', 'shipping_method',
     35        'payment_method', 'shipping_method', 'tracking',
    3636    ];
    3737
    … …  
    134134
    135135        return CommerceResult::success(['downloads' => $downloads]);
     136    }
     137
     138    /**
     139     * Đơn gần nhất của khách đã đăng nhập (2.6.0, S5, tool get_my_orders).
     140     *
     141     * Không cần mã đơn hay email: tài khoản WordPress là bằng chứng. $user_id PHẢI là khách của lượt
     142     * (Support\CustomerContext), không bao giờ lấy từ tham số của model.
     143     */
     144    public function recent_for_customer($user_id, $limit = 5) {
     145        if (!self::woocommerce_active()) {
     146            return CommerceResult::woocommerce_missing();
     147        }
     148        $user_id = (int) $user_id;
     149        if ($user_id <= 0) {
     150            return CommerceResult::forbidden('The customer is not logged in. Ask for the order number and the email or phone number used on the order, then call get_order_status.');
     151        }
     152
     153        $orders = wc_get_orders([
     154            'customer_id' => $user_id,
     155            'type' => 'shop_order',
     156            'status' => function_exists('wc_get_order_statuses') ? array_keys(wc_get_order_statuses()) : 'any',
     157            'limit' => max(1, min(10, (int) $limit)),
     158            'orderby' => 'date',
     159            'order' => 'DESC',
     160        ]);
     161
     162        $out = [];
     163        foreach ((array) $orders as $order) {
     164            // Lớp bảo vệ thứ hai: chỉ đơn đúng của tài khoản này, kể cả khi bộ lọc của truy vấn bị plugin khác đổi.
     165            if (is_object($order) && (int) $order->get_customer_id() === $user_id) {
     166                $out[] = self::present($order);
     167            }
     168        }
     169
     170        return CommerceResult::success(['orders' => $out, 'count' => count($out)]);
    136171    }
    137172
    … …  
    278313            'payment_method' => $order->get_payment_method_title(),
    279314            'shipping_method' => $order->get_shipping_method(),
     315            // Mã vận đơn (S5): thẻ đơn hiện mã mà model không biết thì nó nói "chưa gửi hàng" (staging 25/10).
     316            'tracking' => array_map(function ($t) {
     317                return ['code' => $t['code'], 'carrier' => $t['carrier'], 'shipped' => $t['date']];
     318            }, \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::for_order($order)),
    280319        ];
    281320    }
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/ProductImage.php

    r3761175 r3765552  
    2121    const SIZES = ['large', 'medium_large', 'medium', 'full'];
    2222
     23    /** Thẻ sản phẩm trong chat (2.6.0) hiện ảnh 64 px: bản nhỏ trước, đủ nét trên màn hình 2x. */
     24    const CARD_SIZES = ['thumbnail', 'medium', 'medium_large', 'large', 'full'];
     25
    2326    /**
    24      * @param int $attachment_id ID ảnh đại diện của sản phẩm.
     27     * @param int      $attachment_id ID ảnh đại diện của sản phẩm.
     28     * @param string[] $sizes         Thứ tự cỡ ảnh thử lần lượt.
    2529     * @return string URL dùng được, hoặc chuỗi rỗng khi không có ảnh nào còn trên đĩa.
    2630     */
    27     public static function url($attachment_id) {
     31    public static function url($attachment_id, array $sizes = self::SIZES) {
    2832        $attachment_id = (int) $attachment_id;
    2933        if ($attachment_id <= 0 || !function_exists('wp_get_attachment_image_src')) {
    … …  
    3438        $tried = [];
    3539
    36         foreach (self::SIZES as $size) {
     40        foreach ($sizes as $size) {
    3741            $src = wp_get_attachment_image_src($attachment_id, $size);
    3842            $url = is_array($src) && !empty($src[0]) ? (string) $src[0] : '';
  • ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/ProductService.php

    r3761175 r3765552  
    5757        // Danh mục rỗng khác hẳn với "không tìm thấy". Nói rõ để model khuyên
    5858        // chủ shop đồng bộ, thay vì trả lời khách là cửa hàng không có gì bán.
    59         if (empty($products) && $scanner->count_cached() === 0) {
    60             $meta['warning'] = 'The product catalog has not been synced yet. Go to Ninhle AI Sales Assistant > Settings and run a product sync.';
     59        $data = [
     60            'count' => count($products),
     61            'currency' => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : '',
     62            'products' => $products,
     63        ];
     64        // Starter (S4): nhắc ngay trong kết quả tool, chỗ model đọc trước bước kế tiếp. Chỉ có câu này trong system
     65        // prompt thì AI thật lúc đặt sẵn, lúc lại bảo khách tự chọn trên thẻ (staging 25/10).
     66        if (!empty($products) && class_exists('\\Smartbuy\\SMBCommerce\\Commerce\\Woo\\CartActionService')
     67            && CartActionService::enabled()) {
     68            $meta['guidance'] = 'If the customer has already said which of these products they want and the option (such as size or colour) or quantity, '
     69                . 'do not ask more questions: call propose_cart_action now with type "add", the product_id, their choice in options '
     70                . '(for example {"Size": "M"}) and the quantity.';
     71        }
     72
     73        if (empty($products)) {
     74            if ($scanner->count_cached() === 0) {
     75                $meta['warning'] = 'The product catalog has not been synced yet. Go to Ninhle AI Sales Assistant > Settings and run a product sync.';
     76            } elseif ($keyword !== '') {
     77                // Cứu lượt tìm hụt (2.6.0, S3): gợi ý gần nhất để model hỏi lại một câu, không bịa sản phẩm.
     78                $rescue = self::rescue($keyword, $scanner);
     79                if ($rescue) {
     80                    $data['suggestions'] = $rescue;
     81                }
     82            }
    6183        }
    6284
    6385        // Giá trong danh sách là số trơn: kèm mã tiền tệ để model không tự đoán "$".
    64         return CommerceResult::success(
    65             [
    66                 'count' => count($products),
    67                 'currency' => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : '',
    68                 'products' => $products,
    69             ],
    70             $meta
    71         );
     86        return CommerceResult::success($data, $meta);
     87    }
     88
     89    /** Điểm tối thiểu (0–100) để một danh mục/tag được coi là gần với từ khoá. */
     90    const RESCUE_MIN_SCORE = 45;
     91
     92    /**
     93     * Danh mục và tag sản phẩm gần nhất với từ khoá tìm hụt, cùng tối đa 3 sản phẩm bán chạy còn hàng của
     94     * danh mục đầu. Điểm = độ giống chuỗi (similar_text) + 25 cho mỗi từ chung có từ 3 chữ cái.
     95     *
     96     * @return array|null ['categories' => [...], 'products' => [...], 'note' => ...] hoặc null.
     97     */
     98    public static function rescue($keyword, WooCommerceProductScanner $scanner) {
     99        if (!function_exists('get_terms')) {
     100            return null;
     101        }
     102        $needle = self::fold($keyword);
     103        $needle_words = array_filter(preg_split('/[^\p{L}\p{N}]+/u', $needle, -1, PREG_SPLIT_NO_EMPTY), function ($w) {
     104            return mb_strlen($w, 'UTF-8') >= 3;
     105        });
     106
     107        $scored = [];
     108        foreach (['product_cat', 'product_tag'] as $taxonomy) {
     109            $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => true, 'number' => 300]);
     110            if (!is_array($terms)) {
     111                continue;
     112            }
     113            foreach ($terms as $term) {
     114                if (!is_object($term) || ($term->slug ?? '') === 'uncategorized') {
     115                    continue;
     116                }
     117                $name = html_entity_decode((string) $term->name, ENT_QUOTES, 'UTF-8');
     118                $hay = self::fold($name);
     119                similar_text($needle, $hay, $percent);
     120                $hay_words = preg_split('/[^\p{L}\p{N}]+/u', $hay, -1, PREG_SPLIT_NO_EMPTY);
     121                $shared = 0;
     122                foreach ($needle_words as $word) {
     123                    foreach ($hay_words as $candidate) {
     124                        // "jacket" khớp "jackets", "áo khoác" khớp "khoác": tiền tố chung từ 3 chữ cái.
     125                        if (strpos($candidate, $word) === 0 || (strpos($word, $candidate) === 0 && mb_strlen($candidate, 'UTF-8') >= 3)) {
     126                            $shared++;
     127                            break;
     128                        }
     129                    }
     130                }
     131                $score = $percent + 25 * $shared;
     132                if ($score >= self::RESCUE_MIN_SCORE) {
     133                    $key = $taxonomy . ':' . $name;
     134                    $scored[$key] = ['name' => $name, 'type' => $taxonomy === 'product_cat' ? 'category' : 'tag', 'score' => $score];
     135                }
     136            }
     137        }
     138        if (empty($scored)) {
     139            return null;
     140        }
     141        uasort($scored, function ($a, $b) {
     142            return $b['score'] <=> $a['score'];
     143        });
     144        $top = array_slice(array_values($scored), 0, 3);
     145
     146        $products = [];
     147        foreach ($top as $match) {
     148            if ($match['type'] === 'category') {
     149                $products = $scanner->search_products('', $match['name'], 0, 0, 3, true);
     150                break;
     151            }
     152        }
     153
     154        return [
     155            'categories' => array_map(function ($m) {
     156                return ['name' => $m['name'], 'type' => $m['type']];
     157            }, $top),
     158            'products' => is_array($products) ? $products : [],
     159            'note' => 'No product matched the search exactly. Ask the customer one short clarifying question, and you may suggest these categories or show these products with [[show:ID]]. Never present them as an exact match.',
     160        ];
     161    }
     162
     163    /** Chữ thường, gộp khoảng trắng; giữ dấu tiếng Việt như LocalResolver. */
     164    private static function fold($text) {
     165        return trim((string) preg_replace('/\s+/u', ' ', mb_strtolower((string) $text, 'UTF-8')));
    72166    }
    73167
  • ninhle-ai-sales-assistant/trunk/src/PHP/Conversation/ConversationRepository.php

    r3752682 r3765552  
    3030    }
    3131   
     32    /** Ngôn ngữ khách đang viết (2.6.0, ReplyLanguage::observe). */
     33    public static function set_language($conversation_id, $language) {
     34        global $wpdb;
     35        return $wpdb->update(
     36            $wpdb->prefix . SMBCOMMERCE_PREFIX . 'conversations',
     37            ['language' => substr(sanitize_key((string) $language), 0, 16)],
     38            ['id' => (int) $conversation_id],
     39            ['%s'],
     40            ['%d']
     41        );
     42    }
     43
    3244    public static function create_conversation($contact_id) {
    3345        global $wpdb;
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/Deactivator.php

    r3752682 r3765552  
    1313        wp_clear_scheduled_hook('smbcommerce_run_jobs');
    1414        wp_clear_scheduled_hook('smbcommerce_finalize_outcomes');
     15        // Sales Insights (2.6.0): gán nhãn đêm và bản tin tuần, kể cả đợt nối tiếp.
     16        \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule();
    1517
    1618        flush_rewrite_rules();
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/JobDispatcher.php

    r3752682 r3765552  
    99 * Nối hàng đợi job với phần xử lý thật.
    1010 *
    11  * Không dùng request loopback: nhiều hosting chặn, staging hay có basic-auth,
    12  * và nó bắt buộc phải mở thêm một endpoint công khai. Thay vào đó, chính
    13  * request poll của widget (5 giây/lần) đóng vai worker — luôn chạy được, và
    14  * cron mỗi phút lo nốt hội thoại mà khách đã đóng tab.
     11 * Từ 2.6.0 job AI được AsyncKick đá chạy nền ngay khi khách gửi tin. Request poll
     12 * của widget chỉ còn ĐỌC, trừ ba trường hợp nó tự chạy job của CHÍNH hội thoại
     13 * đó (không bao giờ job của khách khác như run_one() trước đây):
     14 *   - job 'visitor' (câu về giỏ hàng): cần cookie phiên của khách, chạy ngay;
     15 *   - chế độ legacy (chạy nền bị host chặn): chạy ngay, như 2.5.0;
     16 *   - job đã chờ quá GRACE_SECONDS mà nền chưa nhận: chạy bù và ghi nhận.
     17 * Cron mỗi phút lo nốt hội thoại mà khách đã đóng tab.
    1518 */
    1619class JobDispatcher {
    … …  
    1821    const CRON_HOOK = 'smbcommerce_run_jobs';
    1922    const SCHEDULE = 'smbcommerce_every_minute';
     23
     24    /** Poll chạy bù job đã được đá nhưng chưa ai nhận sau ngần này giây. */
     25    const GRACE_SECONDS = 4;
     26
     27    /**
     28     * Khi nền còn sống mà hàng đợi đầy: lưới an toàn, poll chỉ chạy bù sau ngần này giây. 60 (không phải 20):
     29     * thử tải 2 job trên hosting dùng chung cho thấy hàng đợi thường dài quá 20 s, và poll giành job đúng lúc
     30     * quá tải chỉ chuyển việc sang worker của khách (poll p95 6 s). Worker nền làm tiếp và cron mỗi phút vẫn chạy.
     31     */
     32    const CONGESTION_GRACE_SECONDS = 60;
    2033
    2134    public static function init() {
    … …  
    5265        }
    5366
     67        // Chạy trong locale của khách (S2): câu cố định, câu trả lời tại chỗ và chế độ local-only ra đúng
     68        // ngôn ngữ khách khi site có bản dịch.
     69        // Khách đã đăng nhập lúc gửi tin (S5): job chạy nền không có người dùng của khách, và request đang
     70        // chạy job có thể là của người khác (admin lúc WP-Cron chạy). Luôn đặt từ payload, kể cả 0.
     71        \Smartbuy\SMBCommerce\Support\CustomerContext::run((int) ($payload['wp_user_id'] ?? 0), function () use ($conversation_id, $payload) {
     72            \Smartbuy\SMBCommerce\AI\ReplyLanguage::in_conversation_language($conversation_id, function () use ($conversation_id, $payload) {
     73                self::process($conversation_id, $payload);
     74            });
     75        });
     76    }
     77
     78    private static function process($conversation_id, $payload) {
     79        // Lượt chạy nền có ngân sách thời gian riêng (TurnClock): chờ nhà cung cấp lâu hơn, vẫn xong trước khi
     80        // khoá job hết hạn.
     81        if (\Smartbuy\SMBCommerce\Support\RequestContext::is_background()) {
     82            \Smartbuy\SMBCommerce\AI\TurnClock::start_background_turn();
     83        }
    5484        try {
    5585            \Smartbuy\SMBCommerce\AI\AIService::process_user_message(
    … …  
    70100                );
    71101            }
     102        } finally {
     103            \Smartbuy\SMBCommerce\AI\TurnClock::clear();
    72104        }
    73105    }
    … …  
    81113
    82114    /**
    83      * Được gọi từ request poll của widget: chỉ làm một job để request không bị
    84      * kéo dài quá lâu, rồi trả tin nhắn về ngay.
     115     * Lượt poll của một hội thoại: một truy vấn đọc job còn dở; chỉ chạy job khi rơi vào ba trường
     116     * hợp ở đầu lớp.
     117     *
     118     * @return array ['pending' => số job còn dở, 'ran' => đã chạy job không, 'queued' => đang chờ slot]
    85119     */
    86     public static function run_one() {
    87         return JobQueue::run_due(1, JobQueue::ACTION_PROCESS_MESSAGE);
     120    public static function poll_turn($conversation_id) {
     121        $jobs = JobQueue::for_conversation($conversation_id);
     122        $turn = ['pending' => count($jobs), 'ran' => false, 'queued' => false];
     123
     124        if (empty($jobs)) {
     125            return $turn;
     126        }
     127
     128        foreach ($jobs as $job) {
     129            if ($job['status'] === 'running') {
     130                return $turn;
     131            }
     132        }
     133
     134        $job = $jobs[0];
     135        $now = current_time('mysql');
     136        if (strtotime((string) $job['next_attempt_at']) > strtotime($now)) {
     137            // Đang chờ thử lại sau lỗi: không ép chạy sớm.
     138            return $turn;
     139        }
     140
     141        $payload = $job['payload'];
     142        $visitor = !empty($payload['visitor']);
     143        $kicked = !empty($payload['kick']) && $payload['kick'] !== AsyncKick::STRATEGY_LEGACY;
     144
     145        // Nền còn sống: job chờ là do hàng đợi đầy, để worker nền làm (chỉ chạy bù sau CONGESTION_GRACE).
     146        // Nền im lặng: chạy bù sau GRACE và ghi "bỏ lỡ".
     147        $alive = $kicked && !$visitor && AsyncKick::background_alive();
     148        if (!$visitor && $kicked && !AsyncKick::is_legacy()) {
     149            $grace = $alive ? self::CONGESTION_GRACE_SECONDS : self::GRACE_SECONDS;
     150            if (JobQueue::age_seconds($job['created_at'], $now) < $grace) {
     151                $turn['queued'] = $alive && JobQueue::is_saturated();
     152                return $turn;
     153            }
     154        }
     155
     156        $claimed = JobQueue::claim_by_id($job['id']);
     157        if (!$claimed) {
     158            $turn['queued'] = JobQueue::is_saturated();
     159            return $turn;
     160        }
     161
     162        if ($kicked && !$visitor && !$alive) {
     163            AsyncKick::note_missed();
     164        }
     165        JobQueue::execute($claimed);
     166        $turn['ran'] = true;
     167        $turn['pending']--;
     168
     169        return $turn;
    88170    }
    89171}
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/JobQueue.php

    r3747521 r3765552  
    1212 * (có thể mất hàng chục giây khi phải gọi tool nhiều vòng) chạy tách ra.
    1313 *
    14  * Job được rút ra bởi, theo thứ tự nhanh dần tới chậm:
    15  *   1. Request loopback không chờ, bắn ngay sau khi enqueue (nhanh nhất)
    16  *   2. Chính request poll của widget (5 giây/lần) — luôn hoạt động, không phụ
    17  *      thuộc loopback hay cron, nên đây là lớp bảo đảm cuối
    18  *   3. Cron mỗi phút, cho hội thoại khách đã đóng tab
     14 * Job được rút ra bởi (2.6.0, xem AsyncKick và JobDispatcher):
     15 *   1. Worker nền được "đá" ngay sau khi enqueue: chạy tiếp sau khi đã trả phản
     16 *      hồi cho khách, hoặc qua request loopback. Xong job thì làm tiếp job chờ.
     17 *   2. Request poll của CHÍNH hội thoại đó, khi job đã chờ quá thời gian ân hạn
     18 *      (chạy nền bị host chặn) hoặc job cần phiên của khách (câu về giỏ hàng).
     19 *   3. Cron mỗi phút, lưới an toàn cho mọi thứ còn sót.
     20 *
     21 * Mỗi hội thoại chỉ có một job chạy tại một thời điểm: hai câu gửi liền nhau
     22 * không được sinh hai câu trả lời song song.
    1923 */
    2024class JobQueue {
    … …  
    2529    const MAX_ATTEMPTS = 3;
    2630
    27     /** Giới hạn job AI chạy đồng thời trên toàn site, chống cạn kiệt PHP worker. */
    28     const MAX_CONCURRENT = 4;
     31    /**
     32     * Giới hạn mặc định số job AI chạy đồng thời trên toàn site, chống cạn kiệt PHP worker. 2 (không phải 3)
     33     * sau thử tải trên hosting dùng chung 24/10/2026; shop có máy chủ riêng tăng bằng option dưới.
     34     */
     35    const DEFAULT_CONCURRENT = 2;
     36
     37    /** Trần cứng của giới hạn đồng thời, kể cả khi option hay filter đặt cao hơn. */
     38    const MAX_CONCURRENT = 8;
     39
     40    /** Option (không có giao diện, đặt bằng WP-CLI) để chỉnh giới hạn đồng thời. */
     41    const CONCURRENCY_OPTION = 'smbcommerce_ai_max_concurrency';
    2942
    3043    /** Job bị coi là kẹt (worker chết giữa chừng) sau ngần này giây. */
    3144    const STALE_LOCK_SECONDS = 180;
    3245
     46    /**
     47     * Job đánh dấu 'visitor' (câu về giỏ hàng, cần cookie phiên của khách) được nhường cho poll của
     48     * chính khách trong ngần này giây; quá hạn (khách đã đóng tab) thì worker nền và cron mới nhận.
     49     */
     50    const VISITOR_WAIT_SECONDS = 30;
     51
    3352    private static function table() {
    3453        global $wpdb;
    … …  
    3655    }
    3756
     57    /**
     58     * conversation_id luôn là khoá ĐẦU TIÊN của payload: for_conversation() tìm job của một hội thoại
     59     * bằng tiền tố JSON đó mà không cần thêm cột.
     60     *
     61     * @return int ID job mới (0 nếu ghi thất bại).
     62     */
    3863    public static function enqueue($action, $payload = []) {
    3964        global $wpdb;
    4065
     66        $payload = self::conversation_first((array) $payload);
    4167        $now = current_time('mysql');
    4268
    43         $wpdb->insert(self::table(), [
     69        $inserted = $wpdb->insert(self::table(), [
    4470            'action' => $action,
    4571            'payload' => wp_json_encode($payload),
    … …  
    5076        ]);
    5177
    52         return $wpdb->insert_id;
    53     }
    54 
    55     /**
    56      * Giành lấy một job đang chờ. Dùng UPDATE có điều kiện nên hai worker chạy
    57      * song song không thể cùng nhận một job.
     78        // Ghi lỗi thì insert_id vẫn giữ ID của lần ghi trước: không được đá nhầm job của người khác.
     79        return $inserted ? (int) $wpdb->insert_id : 0;
     80    }
     81
     82    /** Giới hạn đồng thời đang áp dụng: option, rồi filter, kẹp trong 1..MAX_CONCURRENT. */
     83    public static function max_concurrent() {
     84        $limit = (int) get_option(self::CONCURRENCY_OPTION, self::DEFAULT_CONCURRENT);
     85        $limit = (int) apply_filters('smbcommerce_ai_max_concurrency', $limit > 0 ? $limit : self::DEFAULT_CONCURRENT);
     86        return max(1, min(self::MAX_CONCURRENT, $limit));
     87    }
     88
     89    /** Đã hết slot chạy đồng thời chưa (job mới sẽ phải xếp hàng). */
     90    public static function is_saturated() {
     91        return self::running_count() >= self::max_concurrent();
     92    }
     93
     94    /**
     95     * Giành lấy job đang chờ lâu nhất. Dùng UPDATE có điều kiện nên hai worker chạy
     96     * song song không thể cùng nhận một job. Bỏ qua job của hội thoại đang có job chạy,
     97     * và job 'visitor' còn trong thời gian nhường cho poll của khách.
    5898     *
    5999     * @return array|null Job đã giành được, hoặc null nếu không có gì để làm.
    … …  
    61101    public static function claim($filter_action = null) {
    62102        global $wpdb;
    63         $table = self::table();
    64103
    65104        self::release_stale_locks();
    66105
    67         if (self::running_count() >= self::MAX_CONCURRENT) {
    68             return null;
    69         }
    70 
    71         $worker_id = wp_generate_uuid4();
    72         $now = current_time('mysql');
     106        if (self::is_saturated()) {
     107            return null;
     108        }
    73109
    74110        // Lọc theo action là tuỳ chọn: chuỗi rỗng nghĩa là mọi action (câu SQL cố định).
    75111        $action = $filter_action !== null ? (string) $filter_action : '';
    76 
    77         $candidate = $wpdb->get_row($wpdb->prepare(
    78             "SELECT id FROM %i
     112        $now = current_time('mysql');
     113
     114        $candidates = $wpdb->get_results($wpdb->prepare(
     115            "SELECT id, payload, created_at FROM %i
    79116             WHERE status = 'pending' AND next_attempt_at <= %s AND (%s = '' OR action = %s)
    80              ORDER BY id ASC LIMIT 1",
    81             $table,
     117             ORDER BY id ASC LIMIT 10",
     118            self::table(),
    82119            $now,
    83120            $action,
    … …  
    85122        ), ARRAY_A);
    86123
    87         if (!$candidate) {
    88             return null;
    89         }
    90 
    91         // Chỉ giành được nếu job vẫn còn 'pending' ở thời điểm UPDATE chạy.
    92         $claimed = $wpdb->query($wpdb->prepare(
    93             "UPDATE %i SET status = 'running', locked_by = %s, updated_at = %s
    94              WHERE id = %d AND status = 'pending'",
    95             $table,
    96             $worker_id,
    97             $now,
    98             $candidate['id']
    99         ));
    100 
    101         if (!$claimed) {
    102             return null;
    103         }
    104 
    105         $job = $wpdb->get_row($wpdb->prepare(
    106             'SELECT * FROM %i WHERE id = %d',
    107             $table,
    108             $candidate['id']
     124        if (empty($candidates)) {
     125            return null;
     126        }
     127
     128        $busy = self::running_conversations();
     129        foreach ($candidates as $candidate) {
     130            $payload = json_decode((string) $candidate['payload'], true) ?: [];
     131            $conversation_id = (int) ($payload['conversation_id'] ?? 0);
     132            if ($conversation_id > 0 && isset($busy[$conversation_id])) {
     133                continue;
     134            }
     135            if (!empty($payload['visitor']) && self::age_seconds($candidate['created_at'], $now) < self::VISITOR_WAIT_SECONDS) {
     136                continue;
     137            }
     138            $job = self::lock((int) $candidate['id']);
     139            if ($job) {
     140                return $job;
     141            }
     142        }
     143
     144        return null;
     145    }
     146
     147    /** Số giây từ $created_at tới $now (cả hai theo giờ site, dạng MySQL). */
     148    public static function age_seconds($created_at, $now = null) {
     149        $now = $now ?: current_time('mysql');
     150        return max(0, strtotime((string) $now) - strtotime((string) $created_at));
     151    }
     152
     153    /**
     154     * Giành đúng một job (worker nền được đá cho job vừa tạo). Cùng điều kiện như claim():
     155     * còn slot, job đang chờ và đã tới hạn, hội thoại không có job khác đang chạy.
     156     *
     157     * @return array|null
     158     */
     159    public static function claim_by_id($job_id) {
     160        global $wpdb;
     161
     162        $job_id = (int) $job_id;
     163        if ($job_id <= 0 || self::is_saturated()) {
     164            return null;
     165        }
     166
     167        $row = $wpdb->get_row($wpdb->prepare(
     168            "SELECT id, payload FROM %i WHERE id = %d AND status = 'pending' AND next_attempt_at <= %s",
     169            self::table(),
     170            $job_id,
     171            current_time('mysql')
    109172        ), ARRAY_A);
    110173
    111         if ($job) {
    112             $job['payload'] = json_decode($job['payload'], true) ?: [];
    113         }
    114 
    115         return $job;
     174        if (!$row) {
     175            return null;
     176        }
     177
     178        $conversation_id = self::payload_conversation($row['payload']);
     179        if ($conversation_id > 0 && isset(self::running_conversations()[$conversation_id])) {
     180            return null;
     181        }
     182
     183        return self::lock($job_id);
     184    }
     185
     186    /**
     187     * Job chưa xong (đang chờ hoặc đang chạy) của một hội thoại, cũ nhất trước. Một truy vấn,
     188     * bảng jobs luôn nhỏ vì job xong là bị xoá.
     189     *
     190     * @return array[] Mỗi phần tử: id, action, status, payload (đã giải mã), next_attempt_at, created_at.
     191     */
     192    public static function for_conversation($conversation_id) {
     193        global $wpdb;
     194
     195        $conversation_id = (int) $conversation_id;
     196        if ($conversation_id <= 0) {
     197            return [];
     198        }
     199
     200        // Tiền tố JSON cố định do enqueue() ghi; dấu phẩy hoặc ngoặc đóng ngay sau số nên hội thoại
     201        // 12 không khớp nhầm 123. Dấu '_' trong LIKE khớp mọi ký tự, kể cả chính nó, nên không cần
     202        // esc_like; kết quả vẫn được lọc lại sau khi giải mã.
     203        $rows = $wpdb->get_results($wpdb->prepare(
     204            "SELECT id, action, status, payload, next_attempt_at, created_at FROM %i
     205             WHERE status IN ('pending', 'running') AND (payload LIKE %s OR payload = %s)
     206             ORDER BY id ASC LIMIT 5",
     207            self::table(),
     208            '{"conversation_id":' . $conversation_id . ',%',
     209            '{"conversation_id":' . $conversation_id . '}'
     210        ), ARRAY_A);
     211
     212        $jobs = [];
     213        foreach ((array) $rows as $row) {
     214            $row['payload'] = json_decode((string) $row['payload'], true) ?: [];
     215            if ((int) ($row['payload']['conversation_id'] ?? 0) === $conversation_id) {
     216                $jobs[] = $row;
     217            }
     218        }
     219        return $jobs;
    116220    }
    117221
    … …  
    151255                'status' => 'pending',
    152256                'locked_by' => null,
    153                 'payload' => wp_json_encode($payload),
     257                'payload' => wp_json_encode(self::conversation_first($payload)),
    154258                'next_attempt_at' => gmdate('Y-m-d H:i:s', strtotime(current_time('mysql')) + $delay),
    155259                'updated_at' => current_time('mysql'),
    … …  
    161265    }
    162266
     267    /** Chạy một job đã giành được: xong thì xoá, lỗi thì hẹn thử lại. */
     268    public static function execute(array $job) {
     269        try {
     270            do_action('smbcommerce_run_job_' . $job['action'], $job['payload'], $job);
     271            self::complete($job['id']);
     272            return true;
     273        } catch (\Throwable $e) {
     274            self::fail($job, $e->getMessage());
     275            return false;
     276        }
     277    }
     278
    163279    /**
    164280     * Rút và chạy tối đa $limit job. Trả về số job đã xử lý.
    … …  
    173289            }
    174290
    175             try {
    176                 do_action('smbcommerce_run_job_' . $job['action'], $job['payload'], $job);
    177                 self::complete($job['id']);
    178             } catch (\Throwable $e) {
    179                 self::fail($job, $e->getMessage());
    180             }
    181 
     291            self::execute($job);
    182292            $processed++;
    183293        }
    … …  
    186296    }
    187297
     298    /** Đổi job sang 'running' nếu nó vẫn còn 'pending' ở thời điểm UPDATE chạy. */
     299    private static function lock($job_id) {
     300        global $wpdb;
     301        $table = self::table();
     302
     303        $claimed = $wpdb->query($wpdb->prepare(
     304            "UPDATE %i SET status = 'running', locked_by = %s, updated_at = %s
     305             WHERE id = %d AND status = 'pending'",
     306            $table,
     307            wp_generate_uuid4(),
     308            current_time('mysql'),
     309            $job_id
     310        ));
     311
     312        if (!$claimed) {
     313            return null;
     314        }
     315
     316        $job = $wpdb->get_row($wpdb->prepare(
     317            'SELECT * FROM %i WHERE id = %d',
     318            $table,
     319            $job_id
     320        ), ARRAY_A);
     321
     322        if ($job) {
     323            $job['payload'] = json_decode($job['payload'], true) ?: [];
     324        }
     325
     326        return $job;
     327    }
     328
    188329    private static function running_count() {
    189330        global $wpdb;
    190331        return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE status = 'running'", self::table()));
     332    }
     333
     334    /** @return array<int,true> Hội thoại đang có job chạy (tối đa MAX_CONCURRENT dòng). */
     335    private static function running_conversations() {
     336        global $wpdb;
     337        $payloads = $wpdb->get_col($wpdb->prepare("SELECT payload FROM %i WHERE status = 'running'", self::table()));
     338
     339        $busy = [];
     340        foreach ((array) $payloads as $payload) {
     341            $conversation_id = self::payload_conversation($payload);
     342            if ($conversation_id > 0) {
     343                $busy[$conversation_id] = true;
     344            }
     345        }
     346        return $busy;
     347    }
     348
     349    private static function payload_conversation($payload) {
     350        $data = is_array($payload) ? $payload : json_decode((string) $payload, true);
     351        return is_array($data) ? (int) ($data['conversation_id'] ?? 0) : 0;
     352    }
     353
     354    private static function conversation_first(array $payload) {
     355        if (!array_key_exists('conversation_id', $payload)) {
     356            return $payload;
     357        }
     358        $payload['conversation_id'] = (int) $payload['conversation_id'];
     359        return ['conversation_id' => $payload['conversation_id']] + $payload;
    191360    }
    192361
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/MigrationManager.php

    r3752682 r3765552  
    1414     * 3 = đo lường 2.4.0: cột chuyển nhân viên và kết quả hội thoại, cột truy vết chi phí AI,
    1515     *     bảng value_events.
     16     * 4 = 2.6.0: conversations.language (ngôn ngữ khách đang viết, S2).
    1617     */
    17     const SCHEMA_REVISION = 3;
     18    const SCHEMA_REVISION = 4;
    1819
    1920    /** Kết quả lần chuyển dữ liệu gần nhất, để chẩn đoán và cho test đọc. */
    … …  
    6465        // Cài mới: model đề xuất (chỉ khi benchmark đã duyệt). Nâng cấp không bao giờ đổi model đã lưu.
    6566        \Smartbuy\SMBCommerce\AI\ModelPolicy::apply_new_install_defaults($previous_version);
     67
     68        // 2.6.0: cài mới trả lời theo ngôn ngữ khách; site nâng cấp giữ hành vi cũ (quyết định 3).
     69        \Smartbuy\SMBCommerce\AI\ReplyLanguage::apply_install_default($previous_version);
    6670
    6771        // Ngày bắt đầu đo của bảng "AI Value & Performance" (UTC): ghi một lần, không bao giờ ghi đè,
    … …  
    151155            'outcome_at'       => 'datetime DEFAULT NULL',
    152156        ]);
     157
     158        // 2.6.0 — ngôn ngữ khách đang viết (S2): chọn ngôn ngữ trả lời và ngôn ngữ nhận giọng nói.
     159        self::add_missing_columns($prefix . 'conversations', [
     160            'language' => 'varchar(16) DEFAULT NULL',
     161        ]);
    153162    }
    154163
    … …  
    208217                outcome varchar(30) DEFAULT NULL,
    209218                outcome_at datetime DEFAULT NULL,
     219                language varchar(16) DEFAULT NULL,
    210220                created_at datetime DEFAULT NULL,
    211221                updated_at datetime DEFAULT NULL,
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/Plugin.php

    r3752682 r3765552  
    4545        }
    4646
     47        // Xin đánh giá WordPress.org khi plugin đã giúp được thật (2.6.0, S10).
     48        if (class_exists('\\Smartbuy\SMBCommerce\\Admin\\ReviewPrompt')) {
     49            \Smartbuy\SMBCommerce\Admin\ReviewPrompt::register();
     50        }
     51
    4752        // Domain-lock notice — chỉ hiển thị khi license.php tồn tại và domain không khớp.
    4853        // Bản phát triển (không có license.php) hoàn toàn im lặng.
    … …  
    7883            \Smartbuy\SMBCommerce\Core\JobDispatcher::init();
    7984        }
     85        if (class_exists('\\Smartbuy\SMBCommerce\\Core\\AsyncKick')) {
     86            \Smartbuy\SMBCommerce\Core\AsyncKick::init();
     87        }
    8088
    8189        if (class_exists('\\Smartbuy\SMBCommerce\\Core\\CronManager')) {
    … …  
    9098            \Smartbuy\SMBCommerce\Analytics\OrderAttribution::init();
    9199        }
     100
     101        // Abilities API (WordPress 6.9+) cho catalog công khai (2.6.0, S8); chủ shop bật tay.
     102        if (class_exists('\\Smartbuy\SMBCommerce\\Abilities\\AbilitiesService')) {
     103            \Smartbuy\SMBCommerce\Abilities\AbilitiesService::init();
     104        }
     105
     106        // Sales Insights (2.6.0, S6): lịch gán nhãn đêm và bản tin tuần (gói Starter).
     107        if (class_exists('\\Smartbuy\SMBCommerce\\Insights\\InsightsService')) {
     108            \Smartbuy\SMBCommerce\Insights\InsightsService::init();
     109        }
    92110    }
    93111}
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/PremiumFallbacks.php

    r3747521 r3765552  
    3030            'Smartbuy\SMBCommerce\\Commerce\\Woo\\VariationService' => NullVariationService::class,
    3131            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CouponService'    => NullCouponService::class,
     32            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CartActionService' => NullCartActionService::class,
    3233            'Smartbuy\SMBCommerce\\Integrations\\TelegramService'   => NullTelegramService::class,
     34            'Smartbuy\SMBCommerce\\Insights\\InsightsLabeler'       => NullInsightsLabeler::class,
     35            'Smartbuy\SMBCommerce\\Insights\\InsightsDigest'        => NullInsightsDigest::class,
     36            'Smartbuy\SMBCommerce\\Commerce\\Woo\\CustomerCard'     => NullCustomerCard::class,
    3337        ];
    3438    }
    … …  
    127131}
    128132
     133/** Bản Free: không có hành động giỏ nâng cao (gói Starter); thẻ giữ nút "Choose options". */
     134class NullCartActionService {
     135    public static function enabled() { return false; }
     136    public static function enrich_card(array $card, $product) { return $card; }
     137    public static function propose(array $params) {
     138        return \Smartbuy\SMBCommerce\Commerce\CommerceResult::not_entitled('cart_actions_advanced');
     139    }
     140    public static function allowlist() { return []; }
     141    public static function prompt_block() { return ''; }
     142    public static function offers_coupons() { return false; }
     143    public static function widget_module_version() { return ''; }
     144    public static function widget_strings() { return []; }
     145}
     146
     147/** Bản Free: Sales Insights không có gán nhãn AI "vì sao không mua" (gói Starter); phần không AI vẫn chạy. */
     148class NullInsightsLabeler {
     149    const FEATURE = 'insights_ai';
     150    const CRON_HOOK = 'smbcommerce_insights_label';
     151    public static function allowed() { return false; }
     152    public static function enabled() { return false; }
     153    public static function run_nightly($mode = '') { return ['stopped' => 'not_entitled']; }
     154    public static function run($limit = 0, $now = null) { return ['stopped' => 'not_entitled']; }
     155    public static function status() {
     156        return ['allowed' => false, 'enabled' => false, 'spent' => 0.0, 'budget' => 0.0, 'model' => '', 'state' => [], 'next_run' => 0];
     157    }
     158    public static function choose_model() { return null; }
     159    public static function spent_this_month() { return 0.0; }
     160    public static function candidates($limit, $now = null) { return []; }
     161    public static function transcript($conversation_id) { return ['text' => '', 'customer' => '']; }
     162    public static function parse($text, $customer_text) { return null; }
     163}
     164
     165/** Bản Free: không có bản tin Sales Insights hằng tuần (gói Starter). */
     166class NullInsightsDigest {
     167    const CRON_HOOK = 'smbcommerce_insights_digest';
     168    public static function enabled() { return false; }
     169    public static function send_weekly() { return false; }
     170    public static function build($now = null) { return null; }
     171    public static function reason_label($reason) { return (string) $reason; }
     172}
     173
     174/** Bản Free: không có thẻ khách trong Inbox (gói Starter). */
     175class NullCustomerCard {
     176    const FEATURE = 'customer_card';
     177    public static function allowed() { return false; }
     178    public static function viewer_can_see_orders() { return false; }
     179    public static function for_conversation($conversation_id) { return null; }
     180}
     181
    129182/** Bản Free: không có thông báo Telegram (gói Starter); lead vẫn được lưu bình thường. */
    130183class NullTelegramService {
  • ninhle-ai-sales-assistant/trunk/src/PHP/Core/Uninstaller.php

    r3752682 r3765552  
    5454        wp_clear_scheduled_hook('smbcommerce_run_jobs');
    5555        wp_clear_scheduled_hook('smbcommerce_finalize_outcomes');
     56        \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule();
    5657
    5758        /*
    … …  
    7778        delete_option('smbcommerce_measurement_started_at');
    7879        delete_option('smbcommerce_outcomes_watermark');
     80        delete_option('smbcommerce_ai_max_concurrency');
     81        delete_option('smbcommerce_async_mode');
     82        delete_option('smbcommerce_async_health');
    7983        delete_transient('smbcommerce_missing_key_notice');
     84        delete_option('smbcommerce_review_prompt');
     85        delete_transient('smbcommerce_wp_ai_available');
    8086    }
    8187
  • ninhle-ai-sales-assistant/trunk/src/PHP/Licensing/LicenseGate.php

    r3752682 r3765552  
    4545        // endpoint chỉ trả trường doanh thu khi can('revenue_attribution').
    4646        'revenue_attribution'  => self::TIER_STARTER,
     47        // Giỏ trong chat nâng cao (2.6.0, S4): chọn biến thể trên thẻ, số lượng, xoá, mã giảm giá cho phép.
     48        'cart_actions_advanced' => self::TIER_STARTER,
     49        // Sales Insights có AI (2.6.0, S6): gán nhãn lý do không mua, bản tin tuần. Phần không AI là Free.
     50        'insights_ai'           => self::TIER_STARTER,
     51        // Thẻ khách trong Inbox (2.6.0, S12): đơn, tổng chi, sản phẩm gần đây của khách đã biết chắc.
     52        'customer_card'         => self::TIER_STARTER,
    4753
    4854        // --- Pro ---
  • ninhle-ai-sales-assistant/trunk/src/PHP/Privacy/PersonalData.php

    r3752682 r3765552  
    185185        }
    186186
     187        $E = '\\Smartbuy\\SMBCommerce\\Analytics\\ValueEvents';
    187188        $events = $wpdb->get_results($wpdb->prepare(
    188189            'SELECT id, event_type, conversation_id, order_id, meta, created_at FROM %i
    189              WHERE event_type IN (%s, %s) AND conversation_id IN (' . implode(',', array_fill(0, count($conversation_ids), '%d')) . ')
     190             WHERE event_type IN (%s, %s, %s, %s, %s) AND conversation_id IN (' . implode(',', array_fill(0, count($conversation_ids), '%d')) . ')
    190191             ORDER BY id ASC',
    191192            array_merge(
    192                 [self::table('value_events'), \Smartbuy\SMBCommerce\Analytics\ValueEvents::ORDER_LINKED, \Smartbuy\SMBCommerce\Analytics\ValueEvents::RESOLUTION_ANSWER],
     193                // Sales Insights (2.6.0): câu hỏi đã che và trích dẫn đã che cũng là chữ của người này.
     194                // Thẻ khách (2.6.0, S12): hội thoại gắn với tài khoản đã đăng nhập hoặc đơn đã chứng minh sở hữu.
     195                [self::table('value_events'), $E::ORDER_LINKED, $E::RESOLUTION_ANSWER, $E::UNANSWERED, $E::NO_PURCHASE_REASON, $E::CUSTOMER_VERIFIED],
    193196                $conversation_ids
    194197            )
    … …  
    198201        foreach ($events as $event) {
    199202            $meta = json_decode((string) $event['meta'], true) ?: [];
    200             $is_order = $event['event_type'] === \Smartbuy\SMBCommerce\Analytics\ValueEvents::ORDER_LINKED;
     203            switch ($event['event_type']) {
     204                case $E::ORDER_LINKED:
     205                    $row = ['name' => __('Order linked to this chat', 'ninhle-ai-sales-assistant'), 'value' => $event['order_id']];
     206                    break;
     207                case $E::UNANSWERED:
     208                    $row = ['name' => __('Question the assistant could not answer (contact details hidden)', 'ninhle-ai-sales-assistant'), 'value' => $meta['question_norm'] ?? ''];
     209                    break;
     210                case $E::CUSTOMER_VERIFIED:
     211                    $row = ['name' => __('Customer identified in this chat', 'ninhle-ai-sales-assistant'), 'value' => !empty($meta['user_id'])
     212                        /* translators: %d: WordPress user ID of the logged-in customer. */
     213                        ? sprintf(__('Logged-in account #%d', 'ninhle-ai-sales-assistant'), (int) $meta['user_id'])
     214                        /* translators: %d: WooCommerce order ID whose ownership the customer proved in the chat. */
     215                        : sprintf(__('Order ID %d (ownership checked)', 'ninhle-ai-sales-assistant'), (int) ($meta['order_id'] ?? 0))];
     216                    break;
     217                case $E::NO_PURCHASE_REASON:
     218                    $row = ['name' => __('Reason the chat did not lead to an order (AI label)', 'ninhle-ai-sales-assistant'),
     219                        'value' => trim(($meta['reason'] ?? '') . (empty($meta['quote']) ? '' : ': "' . $meta['quote'] . '"'))];
     220                    break;
     221                default:
     222                    $row = ['name' => __('Answer to "Did this answer your question?"', 'ninhle-ai-sales-assistant'), 'value' => $meta['answer'] ?? ''];
     223            }
    201224            $items[] = [
    202225                'group_id' => 'smb-commerce-value',
    … …  
    205228                'data' => [
    206229                    ['name' => __('Conversation', 'ninhle-ai-sales-assistant'), 'value' => $event['conversation_id']],
    207                     $is_order
    208                         ? ['name' => __('Order linked to this chat', 'ninhle-ai-sales-assistant'), 'value' => $event['order_id']]
    209                         : ['name' => __('Answer to "Did this answer your question?"', 'ninhle-ai-sales-assistant'), 'value' => $meta['answer'] ?? ''],
     230                    $row,
    210231                    ['name' => __('Recorded (UTC)', 'ninhle-ai-sales-assistant'), 'value' => $event['created_at']],
    211232                ],
  • ninhle-ai-sales-assistant/trunk/src/PHP/REST/AdminController.php

    r3761175 r3765552  
    109109    }
    110110   
     111    /** Một hội thoại (Inbox mở từ Sales Insights). */
     112    public static function get_conversation($request) {
     113        $conversation = ConversationRepository::get_conversation((int) $request->get_param('id'));
     114        if (!$conversation) {
     115            return new \WP_Error('not_found', 'Conversation not found', ['status' => 404]);
     116        }
     117        return rest_ensure_response($conversation);
     118    }
     119
     120    /**
     121     * Thẻ khách của hội thoại (S12, Starter): chỉ khi khách đã biết chắc và người xem có quyền xem đơn
     122     * WooCommerce. Không có gói hoặc không có quyền thì card = null (giao diện không hiện cột).
     123     */
     124    public static function get_customer_card($request) {
     125        $cards = \Smartbuy\SMBCommerce\Commerce\Woo\CustomerCard::class;
     126        if (!$cards::allowed()) {
     127            return rest_ensure_response(['allowed' => false, 'card' => null]);
     128        }
     129        if (!$cards::viewer_can_see_orders()) {
     130            return rest_ensure_response(['allowed' => true, 'can_view_orders' => false, 'card' => null]);
     131        }
     132        return rest_ensure_response([
     133            'allowed' => true,
     134            'can_view_orders' => true,
     135            'card' => $cards::for_conversation((int) $request->get_param('id')),
     136        ]);
     137    }
     138
    111139    public static function delete_conversation($request) {
    112140        if (!current_user_can('manage_smbcommerce')) {
    … …  
    170198        $settings['smbcommerce_font_family'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_font_family', 'system');
    171199        $settings['smbcommerce_ai_provider'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_ai_provider', 'openai');
     200        // Abilities API cho AI agent (S8): WordPress 6.9+.
     201        $settings['smbcommerce_abilities_enabled'] = \Smartbuy\SMBCommerce\Abilities\AbilitiesService::enabled() ? '1' : '0';
     202        $settings['smbcommerce_abilities_supported'] = \Smartbuy\SMBCommerce\Abilities\AbilitiesService::supported();
     203        // Kết nối AI của WordPress 7.0+ (S7): chỉ báo có/không, không bao giờ trả khoá.
     204        $settings['smbcommerce_wp_ai_available'] = \Smartbuy\SMBCommerce\AI\Providers\WordPressAiProvider::available(true);
    172205       
    173206        $settings['smbcommerce_openai_key_exists'] = !empty(\Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_openai_key'));
    … …  
    232265        $settings['smbcommerce_hide_on_checkout'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_hide_on_checkout', '0');
    233266        $settings['smbcommerce_voice_input_enabled'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_voice_input_enabled', '0');
     267        $settings['smbcommerce_reply_language'] = \Smartbuy\SMBCommerce\AI\ReplyLanguage::mode();
     268        $settings['smbcommerce_coupon_allowlist'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_coupon_allowlist', '');
     269        // Mã vận đơn theo meta riêng của plugin vận chuyển (S5); trải ra thành các ô của màn hình cài đặt.
     270        $tracking = \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::map() ?: [];
     271        foreach (['code_meta', 'carrier_meta', 'carrier', 'url_template'] as $field) {
     272            $settings['smbcommerce_tracking_' . $field] = (string) ($tracking[$field] ?? '');
     273        }
    234274
    235275        return rest_ensure_response(['settings' => $settings]);
    … …  
    280320            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_font_family', sanitize_text_field($params['smbcommerce_font_family']));
    281321        }
    282         if (isset($params['smbcommerce_ai_provider'])) {
    283             \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_provider', sanitize_text_field($params['smbcommerce_ai_provider']));
     322        // Chỉ nhận provider plugin biết (thêm 'wordpress' từ 2.6.0, S7); giá trị lạ bị bỏ qua.
     323        if (isset($params['smbcommerce_ai_provider'])
     324            && in_array($params['smbcommerce_ai_provider'], ['openai', 'anthropic', 'gemini', 'wordpress'], true)) {
     325            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_provider', $params['smbcommerce_ai_provider']);
    284326        }
    285327       
    … …  
    397439            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_telegram_link', sanitize_text_field($params['smbcommerce_telegram_link']));
    398440        }
    399         foreach (['smbcommerce_ai_notice_enabled', 'smbcommerce_hide_on_checkout', 'smbcommerce_voice_input_enabled'] as $toggle) {
     441        foreach (['smbcommerce_ai_notice_enabled', 'smbcommerce_hide_on_checkout', 'smbcommerce_voice_input_enabled', 'smbcommerce_abilities_enabled'] as $toggle) {
    400442            if (isset($params[$toggle])) {
    401443                \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting($toggle, (string) $params[$toggle] === '1' ? '1' : '0');
    … …  
    405447            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_notice_text', sanitize_text_field($params['smbcommerce_ai_notice_text']));
    406448        }
     449        // Chỉ hai giá trị; giá trị lạ không được lưu (sẽ bị đọc thành 'customer' và đổi hành vi site nâng cấp).
     450        if (isset($params['smbcommerce_reply_language'])
     451            && in_array($params['smbcommerce_reply_language'], [\Smartbuy\SMBCommerce\AI\ReplyLanguage::MODE_CUSTOMER, \Smartbuy\SMBCommerce\AI\ReplyLanguage::MODE_STORE], true)) {
     452            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_reply_language', $params['smbcommerce_reply_language']);
     453        }
     454
     455        // Starter (S4): mã AI được đề xuất, mỗi dòng "MÃ" hoặc "MÃ, mức giảm tối đa"; tối đa 20 dòng.
     456        if (isset($params['smbcommerce_coupon_allowlist'])) {
     457            $lines = preg_split('/\R/', sanitize_textarea_field((string) $params['smbcommerce_coupon_allowlist']));
     458            $lines = array_slice(array_values(array_filter(array_map('trim', $lines), 'strlen')), 0, 20);
     459            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_coupon_allowlist', implode("\n", $lines));
     460        }
     461
     462        // Mã vận đơn (S5): bốn ô gộp thành một cấu hình đã kiểm; khoá meta sai hoặc trống thì xoá cấu hình.
     463        if (isset($params['smbcommerce_tracking_code_meta'])) {
     464            $map = \Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::sanitize_map([
     465                'code_meta' => (string) $params['smbcommerce_tracking_code_meta'],
     466                'carrier_meta' => (string) ($params['smbcommerce_tracking_carrier_meta'] ?? ''),
     467                'carrier' => (string) ($params['smbcommerce_tracking_carrier'] ?? ''),
     468                'url_template' => (string) ($params['smbcommerce_tracking_url_template'] ?? ''),
     469            ]);
     470            \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting(\Smartbuy\SMBCommerce\Commerce\Tracking\TrackingAdapter::SETTING_MAP, $map ?: '');
     471        }
     472
     473        // Kiểm lại cách chạy nền mỗi lần lưu (như Site Health): host có thể vừa mở hoặc chặn loopback.
     474        \Smartbuy\SMBCommerce\Core\AsyncKick::daily_check();
    407475
    408476        return rest_ensure_response(['success' => true]);
  • ninhle-ai-sales-assistant/trunk/src/PHP/REST/ChatController.php

    r3752682 r3765552  
    1313
    1414class ChatController {
    15    
     15
     16    /** Ý định Local dùng giỏ hàng của khách (LocalHandler::checkout, view_cart): chạy trong poll của khách. */
     17    const VISITOR_INTENTS = ['checkout', 'view_cart'];
     18
    1619    public static function bootstrap($request) {
    1720        $session_token = $request->get_header('X-SMB-Commerce-Session');
    … …  
    4043            $response['status'] = $conversation['status'];
    4144            $response['agent_name'] = self::agent_name($conversation);
     45            $response['language'] = $conversation['language'] ?? null;
    4246           
    4347            $messages = MessageRepository::get_messages($conversation['id'], 0, 50);
    … …  
    108112        }
    109113
     114        // Ngôn ngữ khách đang viết (S2): lưu khi đổi, dùng cho câu trả lời và nhập giọng nói.
     115        \Smartbuy\SMBCommerce\AI\ReplyLanguage::observe($conversation, $content);
     116
     117        // Khách đã đăng nhập (cookie + nonce REST): bằng chứng cho thẻ khách trong Inbox (S12).
     118        \Smartbuy\SMBCommerce\Commerce\CustomerProof::logged_in($conversation_id, get_current_user_id());
     119
    110120        // Hội thoại thật bắt đầu ở tin đầu tiên của khách (mở widget không tính); ghi một lần.
    111121        \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
    … …  
    121131        $is_thinking_mode = rest_sanitize_boolean($request->get_param('thinking_mode'));
    122132
    123         \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE, [
     133        $payload = [
    124134            'conversation_id' => (int) $conversation_id,
    125135            'thinking_mode' => (bool) $is_thinking_mode,
    126         ]);
     136            // Khách đã đăng nhập (cookie + nonce): worker nền không có người dùng hiện tại.
     137            'wp_user_id' => (int) get_current_user_id(),
     138        ];
     139
     140        // Câu về giỏ hàng được trả lời tại chỗ, không gọi AI, nhưng cần cookie phiên của khách:
     141        // để lượt poll kế tiếp của chính khách chạy (ngay sau ~100 ms). Mọi câu khác chạy nền.
     142        $routing = \Smartbuy\SMBCommerce\Commerce\LocalResolver::classify(
     143            $content,
     144            \Smartbuy\SMBCommerce\Commerce\RuntimeMode::current()
     145        );
     146        $visitor = \Smartbuy\SMBCommerce\Commerce\LocalResolver::is_local($routing)
     147            && in_array($routing['intent'] ?? '', self::VISITOR_INTENTS, true);
     148
     149        $strategy = \Smartbuy\SMBCommerce\Core\AsyncKick::strategy();
     150        if ($visitor) {
     151            $payload['visitor'] = true;
     152        } else {
     153            $payload['kick'] = $strategy;
     154        }
     155
     156        $job_id = \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE, $payload);
     157
     158        if (!$visitor && $job_id > 0) {
     159            \Smartbuy\SMBCommerce\Core\AsyncKick::kick($job_id, $strategy);
     160        }
    127161
    128162        return rest_ensure_response($message);
    … …  
    145179        }
    146180
    147         // Request poll này chính là worker của hàng đợi. Nhờ vậy câu trả lời luôn
    148         // được sinh ra mà không cần cron chạy đúng giờ hay loopback hoạt động.
    149         \Smartbuy\SMBCommerce\Core\JobDispatcher::run_one();
     181        // 2.6.0: AI chạy nền, poll chỉ đọc. Nó chỉ tự chạy job của CHÍNH hội thoại này khi job cần
     182        // phiên của khách, khi chạy nền bị host chặn, hoặc khi nền chưa nhận sau thời gian ân hạn.
     183        $turn = \Smartbuy\SMBCommerce\Core\JobDispatcher::poll_turn($conversation_id);
     184
     185        // Tương tác có sản phẩm từ lượt AI chạy nền: giờ mới ghi được vào phiên của khách.
     186        \Smartbuy\SMBCommerce\Analytics\OrderAttribution::flush_deferred($conversation_id);
    150187
    151188        $messages = MessageRepository::get_messages($conversation_id, $last_message_id, 50);
    152189
    153         // Trạng thái đọc lại SAU run_one: lượt xử lý vừa chạy có thể đã chuyển nhân viên.
    154         $conversation = ConversationRepository::get_conversation($conversation_id) ?: $conversation;
     190        // Lượt xử lý vừa chạy ở đây có thể đã chuyển nhân viên: đọc lại trạng thái.
     191        if ($turn['ran']) {
     192            $conversation = ConversationRepository::get_conversation($conversation_id) ?: $conversation;
     193        }
     194
     195        // queued: hết slot chạy đồng thời. Không còn job nào thì không cần đọc giai đoạn.
     196        if ($turn['queued']) {
     197            $stage = 'queued';
     198        } elseif ($turn['pending'] > 0) {
     199            $stage = \Smartbuy\SMBCommerce\AI\AIService::current_stage($conversation_id);
     200        } else {
     201            $stage = null;
     202        }
    155203
    156204        return rest_ensure_response([
    … …  
    160208            'status' => $conversation['status'],
    161209            'agent_name' => self::agent_name($conversation),
    162             'stage' => \Smartbuy\SMBCommerce\AI\AIService::current_stage($conversation_id),
     210            'stage' => $stage,
     211            // Nhập giọng nói nhận theo ngôn ngữ khách đang viết, trước <html lang> của trang.
     212            'language' => $conversation['language'] ?? null,
    163213        ]);
    164214    }
    … …  
    236286
    237287        if ($result['changed']) {
    238             MessageRepository::add_message($conversation_id, 'system', __('Thanks, our team has been notified. A member of staff will reply here as soon as possible.', 'ninhle-ai-sales-assistant'));
     288            \Smartbuy\SMBCommerce\AI\ReplyLanguage::in_conversation_language($conversation_id, function () use ($conversation_id) {
     289                MessageRepository::add_message($conversation_id, 'system', __('Thanks, our team has been notified. A member of staff will reply here as soon as possible.', 'ninhle-ai-sales-assistant'));
     290            });
    239291
    240292            \Smartbuy\SMBCommerce\Webhooks\WebhookDispatcher::send(
    … …  
    273325
    274326    /** Loại sự kiện widget được gửi lên; mọi giá trị khác bị từ chối. */
    275     const WIDGET_EVENTS = ['product_click', 'cart_click', 'resolution_yes', 'resolution_no'];
     327    const WIDGET_EVENTS = ['product_click', 'cart_click', 'cart_add', 'resolution_yes', 'resolution_no'];
    276328
    277329    /**
    … …  
    310362                ['answer' => $type === 'resolution_yes' ? 'yes' : 'no']
    311363            );
     364            // Sales Insights (S6): câu khách vừa hỏi là câu chưa được giải đáp.
     365            if ($recorded && $type === 'resolution_no') {
     366                \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'customer_no');
     367            }
    312368            return rest_ensure_response(['recorded' => $recorded]);
    313369        }
    … …  
    318374        }
    319375        $ref = substr(preg_replace('/[^a-z0-9_-]/i', '', (string) $request->get_param('ref')), 0, 64);
     376
     377        // Thêm vào giỏ trong chat (S4): widget báo SAU khi Store API đã thêm thành công. Một sản phẩm tính
     378        // một lần mỗi hội thoại cho tỉ lệ "chat → thêm vào giỏ".
     379        if ($type === 'cart_add' && ctype_digit($ref)) {
     380            \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
     381                \Smartbuy\SMBCommerce\Analytics\ValueEvents::CART_ADD,
     382                $conversation_id,
     383                'p:' . $ref,
     384                'customer',
     385                null,
     386                ['product_id' => (int) $ref, 'qty' => max(1, min(99, (int) $request->get_param('qty')))]
     387            );
     388        }
     389
    320390        $recorded = \Smartbuy\SMBCommerce\Analytics\ValueEvents::record(
    321391            \Smartbuy\SMBCommerce\Analytics\ValueEvents::QUALIFYING_INTERACTION,
  • ninhle-ai-sales-assistant/trunk/src/PHP/REST/RouteRegistry.php

    r3752682 r3765552  
    99
    1010class RouteRegistry {
     11    /** @var float|null Lúc handler của plugin bắt đầu, cho header Server-Timing. */
     12    private static $started = null;
     13
    1114    public static function init() {
    1215        add_action('rest_api_init', [self::class, 'register_routes']);
     16        add_filter('rest_request_before_callbacks', [self::class, 'start_timer'], 10, 3);
    1317        add_filter('rest_post_dispatch', [self::class, 'no_cache_headers'], 10, 3);
     18    }
     19
     20    public static function start_timer($response, $handler, $request) {
     21        if (strpos((string) $request->get_route(), '/smb-commerce/v1/') === 0) {
     22            self::$started = microtime(true);
     23        }
     24        return $response;
    1425    }
    1526
    … …  
    2233     */
    2334    public static function no_cache_headers($response, $server, $request) {
    24         if (!($response instanceof \WP_REST_Response) || strpos((string) $request->get_route(), '/smb-commerce/v1/') !== 0) {
     35        if (!($response instanceof \WP_REST_Response)) {
     36            return $response;
     37        }
     38        $route = (string) $request->get_route();
     39        if (strpos($route, '/smb-commerce/v1/') !== 0 && !self::is_store_cart_route($route)) {
    2540            return $response;
    2641        }
    … …  
    3247        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- API công khai của plugin LiteSpeed Cache, phải gọi đúng tên của họ.
    3348        do_action('litespeed_control_set_nocache', 'Ninhle AI Sales Assistant REST response');
     49
     50        // Thời gian của riêng handler plugin (không gồm lúc WordPress khởi động), xem được trong
     51        // DevTools > Network > Timing; đo poll nhẹ của S1 (2.6.0) trên host thật.
     52        if (self::$started !== null && strpos((string) $request->get_route(), '/smb-commerce/v1/chat/') === 0) {
     53            // wp = cả request PHP từ lúc máy chủ nhận (gồm khởi động WordPress), để biết chi phí thật của mỗi poll.
     54            $total = isset($_SERVER['REQUEST_TIME_FLOAT']) ? (microtime(true) - (float) $_SERVER['REQUEST_TIME_FLOAT']) * 1000 : 0;
     55            $response->header('Server-Timing', sprintf('smb;dur=%.1f, wp;dur=%.1f', (microtime(true) - self::$started) * 1000, $total));
     56        }
    3457        return $response;
     58    }
     59
     60    /**
     61     * Route giỏ hàng của WooCommerce Store API (/wc/store/v1/cart, …/cart/items…). Giỏ là của RIÊNG từng
     62     * khách nhưng LiteSpeed Cache mặc định cache cả REST GET: trên staging (2026-10-24) GET /wc/store/v1/cart
     63     * trả "hit, public, max-age 7 ngày" bất kể cookie, nên khách thấy giỏ rỗng hoặc giỏ của người khác và
     64     * nhận nonce cũ (hết hạn sau ~1 ngày, "Add to cart" trong chat sẽ lỗi). Route sản phẩm công khai không đụng.
     65     */
     66    public static function is_store_cart_route($route) {
     67        return (bool) preg_match('#^/wc/store(/v\d+)?/cart(/|$)#', (string) $route);
    3568    }
    3669
    … …  
    70103        ]);
    71104
     105        // Worker nền qua loopback (AsyncKick, 2.6.0). Không có phiên hay cookie: quyền là chữ ký
     106        // HMAC gắn với ID job và hạn 60 giây; job 0 là lời gọi kiểm tra loopback.
     107        register_rest_route($namespace, \Smartbuy\SMBCommerce\Core\AsyncKick::ROUTE, [
     108            'methods'  => \WP_REST_Server::CREATABLE,
     109            'callback' => [\Smartbuy\SMBCommerce\Core\AsyncKick::class, 'handle'],
     110            'permission_callback' => [\Smartbuy\SMBCommerce\Core\AsyncKick::class, 'permission'],
     111            'args' => [
     112                'job' => ['type' => 'integer', 'required' => true, 'minimum' => 0],
     113                'exp' => ['type' => 'integer', 'required' => true, 'minimum' => 0],
     114                'sig' => ['type' => 'string', 'required' => true, 'pattern' => '^[a-f0-9]{64}$'],
     115            ],
     116        ]);
     117
    72118        register_rest_route($namespace, '/chat/clear', [
    73119            'methods'  => \WP_REST_Server::CREATABLE,
    … …  
    98144                'type' => Guard::arg_text(true, 32),
    99145                'ref' => Guard::arg_text(false, 64),
     146                // Số lượng của sự kiện cart_add (2.6.0, S4).
     147                'qty' => ['type' => 'integer', 'required' => false, 'minimum' => 1, 'maximum' => 99],
    100148            ],
    101149        ]);
    … …  
    263311        ]);
    264312
     313        // Tab "Sales Insights" (2.6.0, S6; phần AI kiểm gói trong handler).
     314        register_rest_route($namespace, '/admin/insights', [
     315            'methods'  => \WP_REST_Server::READABLE,
     316            'callback' => [InsightsController::class, 'report'],
     317            'permission_callback' => [self::class, 'check_manage'],
     318        ]);
     319        foreach (['answer' => 'answer', 'dismiss' => 'dismiss', 'notes/delete' => 'delete_note', 'settings' => 'save_settings'] as $path => $method) {
     320            register_rest_route($namespace, '/admin/insights/' . $path, [
     321                'methods'  => \WP_REST_Server::CREATABLE,
     322                'callback' => [InsightsController::class, $method],
     323                'permission_callback' => [self::class, 'check_manage'],
     324            ]);
     325        }
     326        // Mở một hội thoại từ Sales Insights trong Inbox.
     327        register_rest_route($namespace, '/admin/conversations/(?P<id>\d+)', [
     328            'methods'  => \WP_REST_Server::READABLE,
     329            'callback' => [AdminController::class, 'get_conversation'],
     330            'permission_callback' => [self::class, 'check_manage'],
     331        ]);
     332        // Thẻ khách trong Inbox (S12, Starter); handler kiểm thêm quyền xem đơn WooCommerce.
     333        register_rest_route($namespace, '/admin/conversations/(?P<id>\d+)/customer', [
     334            'methods'  => \WP_REST_Server::READABLE,
     335            'callback' => [AdminController::class, 'get_customer_card'],
     336            'permission_callback' => [self::class, 'check_view_inbox'],
     337        ]);
     338
    265339        // Bảng "AI Value & Performance" (mọi gói; doanh thu kiểm quyền trong handler).
    266340        register_rest_route($namespace, '/admin/value-metrics', [
    … …  
    317391            '/tools/product-variations' => 'get_product_variations',
    318392            '/tools/check-coupon'       => 'check_coupon',
     393            '/tools/propose-cart-action' => 'propose_cart_action',
    319394            '/tools/order-status'       => 'get_order_status',
     395            '/tools/my-orders'          => 'get_my_orders',
    320396            '/tools/customer-downloads' => 'get_customer_downloads',
    321397            '/tools/policy'             => 'get_policy',
  • ninhle-ai-sales-assistant/trunk/src/PHP/REST/ToolController.php

    r3747521 r3765552  
    5959    }
    6060
     61    /**
     62     * Đề xuất hành động giỏ (Starter, S4): sản phẩm kèm biến thể/số lượng, hoặc mã giảm giá trong danh sách
     63     * cho phép. Không ghi giỏ: chỉ kiểm và dựng thẻ để khách tự bấm.
     64     */
     65    public static function propose_cart_action($request) {
     66        // Lựa chọn bằng chữ ("Size" => "M"): tối đa 5 thuộc tính, chữ trơn.
     67        $options = [];
     68        foreach (array_slice((array) ($request->get_param('options') ?: []), 0, 5, true) as $name => $value) {
     69            if (is_scalar($value)) {
     70                $options[sanitize_text_field((string) $name)] = sanitize_text_field((string) $value);
     71            }
     72        }
     73        return self::respond_flat(\Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::propose([
     74            'type' => sanitize_key((string) $request->get_param('type')),
     75            'product_id' => (int) $request->get_param('product_id'),
     76            'variation_id' => (int) $request->get_param('variation_id'),
     77            'options' => $options,
     78            'quantity' => (int) ($request->get_param('quantity') ?: 1),
     79            'code' => sanitize_text_field((string) $request->get_param('code')),
     80        ]));
     81    }
     82
    6183    public static function get_product_detail($request) {
    6284        $service = new \Smartbuy\SMBCommerce\Commerce\Woo\ProductService();
    … …  
    87109        ];
    88110
    89         // Khách đã đăng nhập thì chính phiên WordPress là bằng chứng.
    90         if (function_exists('get_current_user_id') && get_current_user_id() > 0) {
    91             $proof['user_id'] = get_current_user_id();
     111        // Khách đã đăng nhập thì chính phiên WordPress là bằng chứng: lấy từ khách của lượt (job chạy nền
     112        // không có người dùng của khách).
     113        $customer = \Smartbuy\SMBCommerce\Support\CustomerContext::user_id();
     114        if ($customer > 0) {
     115            $proof['user_id'] = $customer;
    92116        }
    93117
    … …  
    95119
    96120        return self::respond($service->lookup($reference, $proof), 'order');
     121    }
     122
     123    /**
     124     * Đơn gần nhất của khách đã đăng nhập (S5). Người dùng lấy từ khách của lượt, không từ tham số: model
     125     * không chọn được tài khoản nào để xem.
     126     */
     127    public static function get_my_orders($request) {
     128        $service = new \Smartbuy\SMBCommerce\Commerce\Woo\OrderService();
     129
     130        return self::respond_flat($service->recent_for_customer(\Smartbuy\SMBCommerce\Support\CustomerContext::user_id(), 5));
    97131    }
    98132
  • ninhle-ai-sales-assistant/trunk/src/PHP/Widget/WidgetLoader.php

    r3761175 r3765552  
    6363            'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')),
    6464            'nonce' => wp_create_nonce('wp_rest'),
     65            // Khách đã đăng nhập (S5): widget gửi nonce kèm tin nhắn để máy chủ biết tài khoản (đơn của tôi).
     66            // Trang của người đăng nhập thường không bị cache trang; nonce sai thì widget gửi lại như khách vãng lai.
     67            'loggedIn' => function_exists('is_user_logged_in') && is_user_logged_in(),
    6568            'widgetConfig' => self::public_config(),
    6669            // Dịch ở phía PHP để chuỗi vào được file .pot và theo locale WordPress.
    6770            'i18n' => [
    6871                'openChat'           => __('Open chat', 'ninhle-ai-sales-assistant'),
     72                /* translators: %d: number of replies the shopper has not read yet. */
     73                'unreadMessages'     => __('new replies: %d', 'ninhle-ai-sales-assistant'),
    6974                'chatWindow'         => __('Chat with our assistant', 'ninhle-ai-sales-assistant'),
    7075                'cart'               => __('Cart', 'ninhle-ai-sales-assistant'),
    … …  
    109114                'stageOrder'         => __('Looking up your order…', 'ninhle-ai-sales-assistant'),
    110115                'stageHandoff'       => __('Connecting you with a team member…', 'ninhle-ai-sales-assistant'),
     116                'stageQueued'        => __('Waiting in line…', 'ninhle-ai-sales-assistant'),
     117                // Thẻ sản phẩm (2.6.0, S3).
     118                'productsLabel'      => __('Products', 'ninhle-ai-sales-assistant'),
     119                'cardsPrev'          => __('Previous products', 'ninhle-ai-sales-assistant'),
     120                'cardsNext'          => __('Next products', 'ninhle-ai-sales-assistant'),
     121                'cardView'           => __('View', 'ninhle-ai-sales-assistant'),
     122                'stockIn'            => __('In stock', 'ninhle-ai-sales-assistant'),
     123                'stockOut'           => __('Out of stock', 'ninhle-ai-sales-assistant'),
     124                'stockBackorder'     => __('On backorder', 'ninhle-ai-sales-assistant'),
     125                /* translators: %s: date the price was shown, e.g. "24 Oct 2026". */
     126                'priceAt'            => __('Price on %s', 'ninhle-ai-sales-assistant'),
     127                // Giỏ hàng trong chat (2.6.0, S4).
     128                'addToCart'          => __('Add to cart', 'ninhle-ai-sales-assistant'),
     129                'adding'             => __('Adding…', 'ninhle-ai-sales-assistant'),
     130                'chooseOptions'      => __('Choose options', 'ninhle-ai-sales-assistant'),
     131                'addedToCart'        => __('Added to cart', 'ninhle-ai-sales-assistant'),
     132                'viewCart'           => __('View cart', 'ninhle-ai-sales-assistant'),
     133                'checkout'           => __('Checkout', 'ninhle-ai-sales-assistant'),
     134                'cartOutOfStock'     => __('Sorry, this item just sold out.', 'ninhle-ai-sales-assistant'),
     135                'cartLimit'          => __("You can't add more of this item.", 'ninhle-ai-sales-assistant'),
     136                'cartError'          => __("Couldn't add to cart.", 'ninhle-ai-sales-assistant'),
     137                'openProduct'        => __('Open the product page', 'ninhle-ai-sales-assistant'),
     138                'yourCart'           => __('Your cart', 'ninhle-ai-sales-assistant'),
     139                'closeCart'          => __('Close', 'ninhle-ai-sales-assistant'),
     140                'loadingCart'        => __('Loading your cart…', 'ninhle-ai-sales-assistant'),
     141                'cartLoadError'      => __("Couldn't load your cart.", 'ninhle-ai-sales-assistant'),
     142                'cartEmpty'          => __('Your cart is empty.', 'ninhle-ai-sales-assistant'),
     143                'subtotal'           => __('Subtotal', 'ninhle-ai-sales-assistant'),
     144                'discount'           => __('Discount', 'ninhle-ai-sales-assistant'),
     145                // Thẻ đơn hàng (2.6.0, S5).
     146                'ordersLabel'        => __('Your orders', 'ninhle-ai-sales-assistant'),
     147                /* translators: %s: order number. */
     148                'orderTitle'         => __('Order #%s', 'ninhle-ai-sales-assistant'),
     149                /* translators: %d: number of other items in the order. */
     150                'orderMore'          => __('and %d more', 'ninhle-ai-sales-assistant'),
     151                'orderTracking'      => __('Tracking number', 'ninhle-ai-sales-assistant'),
     152                'orderTrack'         => __('Track', 'ninhle-ai-sales-assistant'),
     153                'orderView'          => __('View order', 'ninhle-ai-sales-assistant'),
    111154                'resolutionQuestion' => __('Did this answer your question?', 'ninhle-ai-sales-assistant'),
    112155                'yes'                => __('Yes', 'ninhle-ai-sales-assistant'),
    … …  
    125168                'voiceLanguage'      => __('Your browser cannot recognise this language. Please type your message.', 'ninhle-ai-sales-assistant'),
    126169                'voiceUnavailable'   => __('Voice input is not available right now. Please type your message.', 'ninhle-ai-sales-assistant'),
    127             ],
     170            // Chuỗi của module giao diện Starter (S4); bản Free: mảng rỗng.
     171            ] + \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::widget_strings(),
    128172        ];
    129173
    … …  
    160204            // Giỏ hàng chỉ có khi WooCommerce đang chạy.
    161205            'cartUrl' => function_exists('wc_get_cart_url') ? wc_get_cart_url() : '',
     206            'checkoutUrl' => function_exists('wc_get_checkout_url') ? wc_get_checkout_url() : '',
     207            // Thêm vào giỏ trong chat (2.6.0, S4): trình duyệt của khách gọi Store API của WooCommerce; máy chủ
     208            // plugin không ghi giỏ. Rỗng = dùng link ?add-to-cart= như trước.
     209            'storeApi' => function_exists('WC') && function_exists('rest_url') ? rest_url('wc/store/v1/') : '',
     210            // Module giao diện Starter (biến thể trên thẻ, số lượng, mã giảm giá): phiên bản để widget tải
     211            // widget-pro.bundle.js cạnh bundle của nó; rỗng ở bản Free hoặc khi gói chưa mở.
     212            'proModule' => \Smartbuy\SMBCommerce\Commerce\Woo\CartActionService::widget_module_version(),
    162213            // Thông báo "đang trò chuyện với AI": mặc định bật; rỗng = tắt.
    163214            'aiNotice' => $get('smbcommerce_ai_notice_enabled', '1') === '1'
  • ninhle-ai-sales-assistant/trunk/src/PHP/WooCommerce/WooCommerceProductScanner.php

    r3761175 r3765552  
    3434
    3535        $synced_ids = [];
     36        $fetched = 0;
    3637        $page = 1;
    3738
    … …  
    5253
    5354            foreach ($products as $product) {
     55                // Sản phẩm "Ẩn" (Catalog visibility: Hidden) không vào cache: chủ shop không muốn nó hiện trong
     56                // cửa hàng hay kết quả tìm kiếm, và thẻ sản phẩm (2.6.0, S3) cũng bỏ qua nó. Trước đây AI vẫn tìm
     57                // thấy và giới thiệu (staging 25/10). Lần quét toàn bộ dọn bản đã cache.
     58                if (method_exists($product, 'get_catalog_visibility') && $product->get_catalog_visibility() === 'hidden') {
     59                    continue;
     60                }
    5461                $this->upsert_product($product, $table, $now);
    5562                $synced_ids[] = $product->get_id();
    5663            }
    5764
     65            $fetched += count($products);
    5866            $remaining -= count($products);
    5967            $page++;
    … …  
    6472        }
    6573
    66         // Chỉ dọn khi quét toàn bộ, nếu không sẽ xoá nhầm phần chưa duyệt tới.
    67         if ($full_sync && !empty($synced_ids)) {
     74        // Chỉ dọn khi quét toàn bộ, nếu không sẽ xoá nhầm phần chưa duyệt tới. Có sản phẩm mà tất cả đều ẩn
     75        // thì cũng dọn hết; WooCommerce không trả sản phẩm nào thì giữ nguyên (có thể chỉ là lỗi tạm thời).
     76        if ($full_sync && $fetched > 0) {
     77            $keep = $synced_ids ?: [0];
    6878            $wpdb->query($wpdb->prepare(
    69                 'DELETE FROM %i WHERE product_id NOT IN (' . implode(',', array_fill(0, count($synced_ids), '%d')) . ')',
    70                 array_merge([$table], $synced_ids)
     79                'DELETE FROM %i WHERE product_id NOT IN (' . implode(',', array_fill(0, count($keep), '%d')) . ')',
     80                array_merge([$table], $keep)
    7181            ));
    7282        }
    … …  
    127137
    128138        $price = $product->get_regular_price();
    129         $sale_price = $product->get_sale_price();
     139        // Giá khuyến mãi chỉ khi đang khuyến mãi (lịch giảm giá chưa tới thì chưa tính), và 0 là giá thật:
     140        // sản phẩm giảm còn 0 ₫ từng bị báo giá gốc 725.000 ₫ vì 0 bị coi là "không giảm" (ninhle.com 26/10).
     141        // NULL trong bảng cache = không khuyến mãi.
     142        $sale_price = $product->is_on_sale() ? $product->get_price() : '';
    130143        // Sản phẩm có biến thể (size, màu) không có giá ở sản phẩm cha. Lưu giá hiện hành thấp nhất của
    131144        // các biến thể; để trống thì bot báo "không thấy giá" và bộ lọc theo giá bỏ sót sản phẩm.
    … …  
    215228                $p['name'],
    216229                (!empty($p['price_is_minimum']) ? 'từ ' : '') . $this->format_price($p['final_price']),
    217                 ($p['sale_price'] && $p['sale_price'] != $p['price']) ? ' (gốc ' . $this->format_price($p['price']) . ')' : '',
     230                (isset($p['sale_price']) && $p['sale_price'] !== '' && (float) $p['sale_price'] != (float) $p['price']) ? ' (gốc ' . $this->format_price($p['price']) . ')' : '',
    218231                $this->describe_stock($p['stock_status'], $p['stock_quantity'] ?? null),
    219232                $p['categories'] ?: 'Chưa phân loại',
    … …  
    227240        $context .= "\nCần chi tiết sâu hơn (tính năng, FAQ, thuộc tính) thì gọi tool get_product_detail với ID ở trên.\n";
    228241
     242        $this->context_ids = array_map(fn($p) => (int) $p['id'], $products);
    229243        return $context;
     244    }
     245
     246    /** @var int[] ID sản phẩm của [PRODUCT CATALOG] vừa dựng (để ToolResultLedger::know). */
     247    private $context_ids = [];
     248
     249    /** ID sản phẩm đã đưa vào [PRODUCT CATALOG] ở lần gọi get_context_for_ai() gần nhất. */
     250    public function context_ids() {
     251        return $this->context_ids;
    230252    }
    231253
    … …  
    252274               AND (%s = '' OR search_text LIKE %s OR title LIKE %s OR sku LIKE %s)
    253275               AND (%s = '' OR categories LIKE %s)
    254                AND (%f <= 0 OR CAST(COALESCE(NULLIF(sale_price, 0), price) AS DECIMAL(12,2)) >= %f)
    255                AND (%f <= 0 OR CAST(COALESCE(NULLIF(sale_price, 0), price) AS DECIMAL(12,2)) <= %f)
     276               AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) >= %f)
     277               AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) <= %f)
    256278             ORDER BY relevance DESC, total_sales DESC, cache_synced_at DESC
    257279             LIMIT %d",
    … …  
    274296        ), ARRAY_A);
    275297
     298        // Cả cụm không khớp liền một chỗ (VD "thanh toán QR WooCommerce" với tên "... Thanh Toán QR Tĩnh Cho
     299        // WooCommerce", ninhle.com 25/10): tìm theo từng từ, xếp theo số từ khớp.
     300        if (!$results && $has_query) {
     301            $results = $this->search_by_words($query, $category_like, $budget_min, $budget_max, (int) $limit, $in_stock_only);
     302        }
     303
    276304        return array_map([$this, 'format_row'], $results ?: []);
     305    }
     306
     307    /** Từ quá phổ biến, không giúp phân biệt sản phẩm. */
     308    const STOP_WORDS = ['cho', 'của', 'và', 'có', 'không', 'là', 'với', 'các', 'những', 'nào', 'shop', 'the', 'for', 'and', 'with', 'of', 'to', 'in', 'a', 'an'];
     309
     310    /** Các từ đáng tìm của một câu: chữ thường, từ 2 ký tự, bỏ từ phổ biến, tối đa 8 từ. */
     311    public static function query_words($query) {
     312        $words = preg_split('/[^\p{L}\p{N}]+/u', mb_strtolower((string) $query, 'UTF-8'), -1, PREG_SPLIT_NO_EMPTY) ?: [];
     313        $words = array_filter($words, fn($w) => mb_strlen($w, 'UTF-8') >= 2 && !in_array($w, self::STOP_WORDS, true));
     314        return array_slice(array_values(array_unique($words)), 0, 8);
     315    }
     316
     317    /**
     318     * Tìm theo từng từ: mỗi từ khớp tên, SKU hoặc search_text được một điểm. Câu 1–2 từ cần khớp hết; câu dài
     319     * hơn được thiếu một từ (khách hay thêm chữ thừa). Cùng bộ lọc danh mục, giá, còn hàng như tìm theo cụm.
     320     */
     321    private function search_by_words($query, $category_like, $budget_min, $budget_max, $limit, $in_stock_only) {
     322        global $wpdb;
     323        $words = self::query_words($query);
     324        if (count($words) < 2) {
     325            return [];
     326        }
     327        $score = [];
     328        $args = [];
     329        foreach ($words as $word) {
     330            $like = '%' . $wpdb->esc_like($word) . '%';
     331            $score[] = '(CASE WHEN search_text LIKE %s OR title LIKE %s OR sku LIKE %s THEN 1 ELSE 0 END)';
     332            array_push($args, $like, $like, $like);
     333        }
     334        $need = count($words) <= 2 ? count($words) : count($words) - 1;
     335        $sql = 'SELECT * FROM (
     336                    SELECT product_id, title, sku, product_type, price, sale_price, stock_status, stock_quantity,
     337                           short_description, categories, permalink, featured_image, total_sales, cache_synced_at,
     338                           ' . implode(' + ', $score) . ' AS matched
     339                    FROM %i
     340                    WHERE (%d = 0 OR stock_status = \'instock\')
     341                      AND (%s = \'\' OR categories LIKE %s)
     342                      AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) >= %f)
     343                      AND (%f <= 0 OR CAST(COALESCE(sale_price, price) AS DECIMAL(12,2)) <= %f)
     344                ) AS candidates
     345                WHERE matched >= %d
     346                ORDER BY matched DESC, total_sales DESC, cache_synced_at DESC
     347                LIMIT %d';
     348        $args = array_merge($args, [
     349            $this->table(), $in_stock_only ? 1 : 0, $category_like, $category_like,
     350            $budget_min, $budget_min, $budget_max, $budget_max, $need, $limit,
     351        ]);
     352        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql chỉ ghép các mẫu %s cố định; mọi giá trị qua prepare().
     353        return $wpdb->get_results($wpdb->prepare($sql, $args), ARRAY_A) ?: [];
    277354    }
    278355
    … …  
    313390            'price'             => $row['price'],
    314391            'sale_price'        => $row['sale_price'],
    315             'final_price'       => ($row['sale_price'] > 0) ? $row['sale_price'] : $row['price'],
     392            // NULL = không khuyến mãi; "0.00" là giá khuyến mãi thật (miễn phí).
     393            'final_price'       => (isset($row['sale_price']) && $row['sale_price'] !== '') ? $row['sale_price'] : $row['price'],
    316394            // Có biến thể: price là giá thấp nhất; kèm khoảng giá thật để model không gán giá thấp nhất
    317395            // cho một size/màu cụ thể.
  • ninhle-ai-sales-assistant/trunk/vite.config.ts

    r3761175 r3765552  
    44import { resolve } from 'path';
    55
    6 // Build hai đầu ra từ mã nguồn trong src/JS:
    7 //  - widget: khung chat ngoài trang (Preact)
    8 //  - admin : giao diện quản trị (JSX qua plugin React, alias sang Preact)
     6// Build ba đầu ra từ mã nguồn trong src/JS:
     7//  - widget    : khung chat ngoài trang (Preact)
     8//  - widget-pro: module giao diện gói Starter (2.6.0), widget tự tải cạnh bundle của nó. Chỉ có trong gói bán:
     9//                build.ps1 -Target wporg loại file này, mã nguồn src/JS/widget-pro và mục của nó trong manifest.
     10//  - admin     : giao diện quản trị (JSX qua plugin React, alias sang Preact)
    911// Chạy: npm ci && npm run build  →  assets/dist/
    1012// build.ps1 chép assets/dist/.vite/manifest.json ra assets/dist/manifest.json khi đóng gói.
    … …  
    1416    plugins: [
    1517      preact({
    16         include: /widget\/.*\.tsx?$/,
     18        include: /widget(-pro)?\/.*\.tsx?$/,
    1719      }),
    1820      react({
    … …  
    3032      manifest: true,
    3133      rollupOptions: {
     34        // Vite (bản build ứng dụng) mặc định bỏ export của entry; widget-pro là module widget import động nên
     35        // phải giữ export. Entry không export gì (widget, admin) không đổi.
     36        preserveEntrySignatures: 'exports-only',
    3237        input: {
    3338          widget: resolve(__dirname, 'src/JS/widget/index.tsx'),
    34           admin: resolve(__dirname, 'src/JS/admin/index.tsx')
     39          'widget-pro': resolve(__dirname, 'src/JS/widget-pro/index.tsx'),
     40          admin: resolve(__dirname, 'src/JS/admin/index.tsx'),
     41          // Tab Sales Insights (2.6.0): admin nạp bằng URL khi mở tab (insightsLoader.ts); React dùng chung qua chunk chung.
     42          'admin-insights': resolve(__dirname, 'src/JS/admin/insights/index.tsx')
    3543        },
    3644        output: {
Note: See TracChangeset for help on using the changeset viewer.