Changeset 3765552
- Timestamp:
- 10/26/2026 04:50:29 AM (63 minutes ago)
- Location:
- ninhle-ai-sales-assistant
- Files:
-
- 96 added
- 6 deleted
- 112 edited
- 1 copied
-
assets/blueprints (added)
-
assets/blueprints/blueprint.json (added)
-
tags/2.6.0 (copied) (copied from ninhle-ai-sales-assistant/trunk )
-
tags/2.6.0/assets/dist/admin-insights.bundle.js (added)
-
tags/2.6.0/assets/dist/admin.bundle.js (modified) (1 diff)
-
tags/2.6.0/assets/dist/admin.css (modified) (1 diff)
-
tags/2.6.0/assets/dist/assets/CartPanel-D6-1EaTY.js (added)
-
tags/2.6.0/assets/dist/assets/ChatWindow-BtSh_sym.js (deleted)
-
tags/2.6.0/assets/dist/assets/ChatWindow-D3sL_SAs.js (added)
-
tags/2.6.0/assets/dist/assets/ProductCards-81V8biLP.js (added)
-
tags/2.6.0/assets/dist/assets/cart-BtM4a74e.js (added)
-
tags/2.6.0/assets/dist/assets/i18n-D_zElFbw.js (added)
-
tags/2.6.0/assets/dist/assets/pro-CgluFZP2.js (added)
-
tags/2.6.0/assets/dist/assets/voice-DkonJhm8.js (added)
-
tags/2.6.0/assets/dist/assets/voice-DtEIuG21.js (deleted)
-
tags/2.6.0/assets/dist/assets/widget-shared-BrY9zvIQ.js (deleted)
-
tags/2.6.0/assets/dist/assets/widget-shared-CHYtJpo8.js (added)
-
tags/2.6.0/assets/dist/manifest.json (modified) (1 diff)
-
tags/2.6.0/assets/dist/widget.bundle.js (modified) (1 diff)
-
tags/2.6.0/assets/dist/widget.css (modified) (1 diff)
-
tags/2.6.0/languages/ninhle-ai-sales-assistant.pot (modified) (25 diffs)
-
tags/2.6.0/ninhle-ai-sales-assistant.php (modified) (2 diffs)
-
tags/2.6.0/package-lock.json (modified) (2 diffs)
-
tags/2.6.0/package.json (modified) (1 diff)
-
tags/2.6.0/readme.txt (modified) (14 diffs)
-
tags/2.6.0/src/JS/admin/components/CustomerCard.tsx (added)
-
tags/2.6.0/src/JS/admin/components/Inbox.tsx (modified) (9 diffs)
-
tags/2.6.0/src/JS/admin/components/Settings.tsx (modified) (8 diffs)
-
tags/2.6.0/src/JS/admin/i18n.ts (added)
-
tags/2.6.0/src/JS/admin/index.tsx (modified) (5 diffs)
-
tags/2.6.0/src/JS/admin/insights (added)
-
tags/2.6.0/src/JS/admin/insights/index.tsx (added)
-
tags/2.6.0/src/JS/admin/insightsLoader.ts (added)
-
tags/2.6.0/src/JS/admin/style.css (modified) (2 diffs)
-
tags/2.6.0/src/JS/widget/api.ts (modified) (2 diffs)
-
tags/2.6.0/src/JS/widget/cards.ts (added)
-
tags/2.6.0/src/JS/widget/cart.ts (added)
-
tags/2.6.0/src/JS/widget/components/CartPanel.tsx (added)
-
tags/2.6.0/src/JS/widget/components/ChatButton.tsx (modified) (3 diffs)
-
tags/2.6.0/src/JS/widget/components/ChatWindow.tsx (modified) (16 diffs)
-
tags/2.6.0/src/JS/widget/components/ProductCards.tsx (added)
-
tags/2.6.0/src/JS/widget/i18n.ts (modified) (2 diffs)
-
tags/2.6.0/src/JS/widget/index.tsx (modified) (3 diffs)
-
tags/2.6.0/src/JS/widget/peek.ts (added)
-
tags/2.6.0/src/JS/widget/pollSchedule.ts (added)
-
tags/2.6.0/src/JS/widget/pro.ts (added)
-
tags/2.6.0/src/JS/widget/style.css (modified) (2 diffs)
-
tags/2.6.0/src/JS/widget/voice.ts (modified) (1 diff)
-
tags/2.6.0/src/PHP/AI/AIProviderFactory.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/AI/AIService.php (modified) (19 diffs)
-
tags/2.6.0/src/PHP/AI/LanguageDetector.php (added)
-
tags/2.6.0/src/PHP/AI/Providers/ClaudeProvider.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/AI/Providers/GeminiProvider.php (modified) (7 diffs)
-
tags/2.6.0/src/PHP/AI/Providers/OpenAIProvider.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/AI/Providers/WordPressAiProvider.php (added)
-
tags/2.6.0/src/PHP/AI/ReplyLanguage.php (added)
-
tags/2.6.0/src/PHP/AI/ResponseSanitizer.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/AI/SalesPlaybook.php (modified) (3 diffs)
-
tags/2.6.0/src/PHP/AI/ToolExecutor.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/AI/ToolResultLedger.php (added)
-
tags/2.6.0/src/PHP/AI/ToolsHelper.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/AI/TurnClock.php (added)
-
tags/2.6.0/src/PHP/Abilities (added)
-
tags/2.6.0/src/PHP/Abilities/AbilitiesService.php (added)
-
tags/2.6.0/src/PHP/Admin/AdminMenu.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Admin/ReviewPrompt.php (added)
-
tags/2.6.0/src/PHP/Analytics/OrderAttribution.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/Analytics/ValueEvents.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/Commerce/CardBuilder.php (added)
-
tags/2.6.0/src/PHP/Commerce/CustomerProof.php (added)
-
tags/2.6.0/src/PHP/Commerce/LocalHandler.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Commerce/LocalResolver.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/Commerce/Tracking (added)
-
tags/2.6.0/src/PHP/Commerce/Tracking/GenericMetaMap.php (added)
-
tags/2.6.0/src/PHP/Commerce/Tracking/ShipmentTrackingMeta.php (added)
-
tags/2.6.0/src/PHP/Commerce/Tracking/TrackingAdapter.php (added)
-
tags/2.6.0/src/PHP/Commerce/Woo/CartService.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Commerce/Woo/OrderService.php (modified) (3 diffs)
-
tags/2.6.0/src/PHP/Commerce/Woo/ProductImage.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Commerce/Woo/ProductService.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/Conversation/ConversationRepository.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/Core/AsyncKick.php (added)
-
tags/2.6.0/src/PHP/Core/Deactivator.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/Core/JobDispatcher.php (modified) (5 diffs)
-
tags/2.6.0/src/PHP/Core/JobQueue.php (modified) (10 diffs)
-
tags/2.6.0/src/PHP/Core/MigrationManager.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/Core/Plugin.php (modified) (3 diffs)
-
tags/2.6.0/src/PHP/Core/PremiumFallbacks.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Core/Uninstaller.php (modified) (2 diffs)
-
tags/2.6.0/src/PHP/Insights (added)
-
tags/2.6.0/src/PHP/Insights/ClusterIndex.php (added)
-
tags/2.6.0/src/PHP/Insights/InsightText.php (added)
-
tags/2.6.0/src/PHP/Insights/InsightsAggregator.php (added)
-
tags/2.6.0/src/PHP/Insights/InsightsRecorder.php (added)
-
tags/2.6.0/src/PHP/Insights/InsightsService.php (added)
-
tags/2.6.0/src/PHP/Insights/InsightsStore.php (added)
-
tags/2.6.0/src/PHP/Licensing/LicenseGate.php (modified) (1 diff)
-
tags/2.6.0/src/PHP/Privacy/PersonalData.php (modified) (3 diffs)
-
tags/2.6.0/src/PHP/REST/AdminController.php (modified) (6 diffs)
-
tags/2.6.0/src/PHP/REST/ChatController.php (modified) (10 diffs)
-
tags/2.6.0/src/PHP/REST/InsightsController.php (added)
-
tags/2.6.0/src/PHP/REST/RouteRegistry.php (modified) (7 diffs)
-
tags/2.6.0/src/PHP/REST/ToolController.php (modified) (3 diffs)
-
tags/2.6.0/src/PHP/Support/CustomerContext.php (added)
-
tags/2.6.0/src/PHP/Support/LightMasker.php (added)
-
tags/2.6.0/src/PHP/Support/RequestContext.php (added)
-
tags/2.6.0/src/PHP/Widget/WidgetLoader.php (modified) (4 diffs)
-
tags/2.6.0/src/PHP/WooCommerce/WooCommerceProductScanner.php (modified) (9 diffs)
-
tags/2.6.0/vite.config.ts (modified) (3 diffs)
-
trunk/assets/dist/admin-insights.bundle.js (added)
-
trunk/assets/dist/admin.bundle.js (modified) (1 diff)
-
trunk/assets/dist/admin.css (modified) (1 diff)
-
trunk/assets/dist/assets/CartPanel-D6-1EaTY.js (added)
-
trunk/assets/dist/assets/ChatWindow-BtSh_sym.js (deleted)
-
trunk/assets/dist/assets/ChatWindow-D3sL_SAs.js (added)
-
trunk/assets/dist/assets/ProductCards-81V8biLP.js (added)
-
trunk/assets/dist/assets/cart-BtM4a74e.js (added)
-
trunk/assets/dist/assets/i18n-D_zElFbw.js (added)
-
trunk/assets/dist/assets/pro-CgluFZP2.js (added)
-
trunk/assets/dist/assets/voice-DkonJhm8.js (added)
-
trunk/assets/dist/assets/voice-DtEIuG21.js (deleted)
-
trunk/assets/dist/assets/widget-shared-BrY9zvIQ.js (deleted)
-
trunk/assets/dist/assets/widget-shared-CHYtJpo8.js (added)
-
trunk/assets/dist/manifest.json (modified) (1 diff)
-
trunk/assets/dist/widget.bundle.js (modified) (1 diff)
-
trunk/assets/dist/widget.css (modified) (1 diff)
-
trunk/languages/ninhle-ai-sales-assistant.pot (modified) (25 diffs)
-
trunk/ninhle-ai-sales-assistant.php (modified) (2 diffs)
-
trunk/package-lock.json (modified) (2 diffs)
-
trunk/package.json (modified) (1 diff)
-
trunk/readme.txt (modified) (14 diffs)
-
trunk/src/JS/admin/components/CustomerCard.tsx (added)
-
trunk/src/JS/admin/components/Inbox.tsx (modified) (9 diffs)
-
trunk/src/JS/admin/components/Settings.tsx (modified) (8 diffs)
-
trunk/src/JS/admin/i18n.ts (added)
-
trunk/src/JS/admin/index.tsx (modified) (5 diffs)
-
trunk/src/JS/admin/insights (added)
-
trunk/src/JS/admin/insights/index.tsx (added)
-
trunk/src/JS/admin/insightsLoader.ts (added)
-
trunk/src/JS/admin/style.css (modified) (2 diffs)
-
trunk/src/JS/widget/api.ts (modified) (2 diffs)
-
trunk/src/JS/widget/cards.ts (added)
-
trunk/src/JS/widget/cart.ts (added)
-
trunk/src/JS/widget/components/CartPanel.tsx (added)
-
trunk/src/JS/widget/components/ChatButton.tsx (modified) (3 diffs)
-
trunk/src/JS/widget/components/ChatWindow.tsx (modified) (16 diffs)
-
trunk/src/JS/widget/components/ProductCards.tsx (added)
-
trunk/src/JS/widget/i18n.ts (modified) (2 diffs)
-
trunk/src/JS/widget/index.tsx (modified) (3 diffs)
-
trunk/src/JS/widget/peek.ts (added)
-
trunk/src/JS/widget/pollSchedule.ts (added)
-
trunk/src/JS/widget/pro.ts (added)
-
trunk/src/JS/widget/style.css (modified) (2 diffs)
-
trunk/src/JS/widget/voice.ts (modified) (1 diff)
-
trunk/src/PHP/AI/AIProviderFactory.php (modified) (4 diffs)
-
trunk/src/PHP/AI/AIService.php (modified) (19 diffs)
-
trunk/src/PHP/AI/LanguageDetector.php (added)
-
trunk/src/PHP/AI/Providers/ClaudeProvider.php (modified) (2 diffs)
-
trunk/src/PHP/AI/Providers/GeminiProvider.php (modified) (7 diffs)
-
trunk/src/PHP/AI/Providers/OpenAIProvider.php (modified) (2 diffs)
-
trunk/src/PHP/AI/Providers/WordPressAiProvider.php (added)
-
trunk/src/PHP/AI/ReplyLanguage.php (added)
-
trunk/src/PHP/AI/ResponseSanitizer.php (modified) (1 diff)
-
trunk/src/PHP/AI/SalesPlaybook.php (modified) (3 diffs)
-
trunk/src/PHP/AI/ToolExecutor.php (modified) (1 diff)
-
trunk/src/PHP/AI/ToolResultLedger.php (added)
-
trunk/src/PHP/AI/ToolsHelper.php (modified) (4 diffs)
-
trunk/src/PHP/AI/TurnClock.php (added)
-
trunk/src/PHP/Abilities (added)
-
trunk/src/PHP/Abilities/AbilitiesService.php (added)
-
trunk/src/PHP/Admin/AdminMenu.php (modified) (2 diffs)
-
trunk/src/PHP/Admin/ReviewPrompt.php (added)
-
trunk/src/PHP/Analytics/OrderAttribution.php (modified) (4 diffs)
-
trunk/src/PHP/Analytics/ValueEvents.php (modified) (1 diff)
-
trunk/src/PHP/Commerce/CardBuilder.php (added)
-
trunk/src/PHP/Commerce/CustomerProof.php (added)
-
trunk/src/PHP/Commerce/LocalHandler.php (modified) (2 diffs)
-
trunk/src/PHP/Commerce/LocalResolver.php (modified) (4 diffs)
-
trunk/src/PHP/Commerce/Tracking (added)
-
trunk/src/PHP/Commerce/Tracking/GenericMetaMap.php (added)
-
trunk/src/PHP/Commerce/Tracking/ShipmentTrackingMeta.php (added)
-
trunk/src/PHP/Commerce/Tracking/TrackingAdapter.php (added)
-
trunk/src/PHP/Commerce/Woo/CartService.php (modified) (2 diffs)
-
trunk/src/PHP/Commerce/Woo/OrderService.php (modified) (3 diffs)
-
trunk/src/PHP/Commerce/Woo/ProductImage.php (modified) (2 diffs)
-
trunk/src/PHP/Commerce/Woo/ProductService.php (modified) (1 diff)
-
trunk/src/PHP/Conversation/ConversationRepository.php (modified) (1 diff)
-
trunk/src/PHP/Core/AsyncKick.php (added)
-
trunk/src/PHP/Core/Deactivator.php (modified) (1 diff)
-
trunk/src/PHP/Core/JobDispatcher.php (modified) (5 diffs)
-
trunk/src/PHP/Core/JobQueue.php (modified) (10 diffs)
-
trunk/src/PHP/Core/MigrationManager.php (modified) (4 diffs)
-
trunk/src/PHP/Core/Plugin.php (modified) (3 diffs)
-
trunk/src/PHP/Core/PremiumFallbacks.php (modified) (2 diffs)
-
trunk/src/PHP/Core/Uninstaller.php (modified) (2 diffs)
-
trunk/src/PHP/Insights (added)
-
trunk/src/PHP/Insights/ClusterIndex.php (added)
-
trunk/src/PHP/Insights/InsightText.php (added)
-
trunk/src/PHP/Insights/InsightsAggregator.php (added)
-
trunk/src/PHP/Insights/InsightsRecorder.php (added)
-
trunk/src/PHP/Insights/InsightsService.php (added)
-
trunk/src/PHP/Insights/InsightsStore.php (added)
-
trunk/src/PHP/Licensing/LicenseGate.php (modified) (1 diff)
-
trunk/src/PHP/Privacy/PersonalData.php (modified) (3 diffs)
-
trunk/src/PHP/REST/AdminController.php (modified) (6 diffs)
-
trunk/src/PHP/REST/ChatController.php (modified) (10 diffs)
-
trunk/src/PHP/REST/InsightsController.php (added)
-
trunk/src/PHP/REST/RouteRegistry.php (modified) (7 diffs)
-
trunk/src/PHP/REST/ToolController.php (modified) (3 diffs)
-
trunk/src/PHP/Support/CustomerContext.php (added)
-
trunk/src/PHP/Support/LightMasker.php (added)
-
trunk/src/PHP/Support/RequestContext.php (added)
-
trunk/src/PHP/Widget/WidgetLoader.php (modified) (4 diffs)
-
trunk/src/PHP/WooCommerce/WooCommerceProductScanner.php (modified) (9 diffs)
-
trunk/vite.config.ts (modified) (3 diffs)
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:1 import{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: 2 2 Help me choose a plan 3 3 Do 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,{})); 4 What 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 5 FREESHIP`,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-c ommerce-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 1 1 { 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 } 57 116 } -
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);1 const __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]); 2 import{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 3 3 msgid "" 4 4 msgstr "" 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" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ninhle-ai-sales-assistant\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-10-2 3T06:16:45+00:00\n"12 "POT-Creation-Date: 2026-10-26T03:45:21+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 17 17 #. Plugin Name of the plugin 18 18 #: ninhle-ai-sales-assistant.php 19 msgid "Ninhle AI Sales Assistant for WooCommerce"19 msgid "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce" 20 20 msgstr "" 21 21 22 22 #. Description of the plugin 23 23 #: 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."24 msgid "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." 25 25 msgstr "" 26 26 … … 35 35 msgstr "" 36 36 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 40 msgid "%s%% off" 41 msgstr "" 42 43 #. translators: %s: discount amount with currency. 44 #: premium/Commerce/Woo/CartActionService.php:381 45 #, php-format 46 msgid "%s off each item" 47 msgstr "" 48 49 #. translators: %s: discount amount with currency. 50 #: premium/Commerce/Woo/CartActionService.php:384 51 #, php-format 52 msgid "%s off your order" 53 msgstr "" 54 55 #: premium/Commerce/Woo/CartActionService.php:471 56 msgid "Quantity" 57 msgstr "" 58 59 #: premium/Commerce/Woo/CartActionService.php:472 60 msgid "Decrease quantity" 61 msgstr "" 62 63 #: premium/Commerce/Woo/CartActionService.php:473 64 msgid "Increase quantity" 65 msgstr "" 66 67 #. translators: %s: product name. 68 #: premium/Commerce/Woo/CartActionService.php:475 69 #, php-format 70 msgid "Remove %s" 71 msgstr "" 72 73 #: premium/Commerce/Woo/CartActionService.php:476 74 msgid "Remove" 75 msgstr "" 76 77 #: premium/Commerce/Woo/CartActionService.php:477 78 msgid "Choose your options to add this to the cart." 79 msgstr "" 80 81 #: premium/Commerce/Woo/CartActionService.php:478 82 msgid "sold out" 83 msgstr "" 84 85 #: premium/Commerce/Woo/CartActionService.php:479 86 msgid "This combination is not available." 87 msgstr "" 88 89 #. translators: %d: largest quantity that can be added. 90 #: premium/Commerce/Woo/CartActionService.php:481 91 #, php-format 92 msgid "Up to %d" 93 msgstr "" 94 95 #: premium/Commerce/Woo/CartActionService.php:482 96 msgid "Updating your cart…" 97 msgstr "" 98 99 #: premium/Commerce/Woo/CartActionService.php:483 100 msgid "Couldn't update your cart." 101 msgstr "" 102 103 #: premium/Commerce/Woo/CartActionService.php:484 104 msgid "Apply" 105 msgstr "" 106 107 #: premium/Commerce/Woo/CartActionService.php:485 108 msgid "Applying…" 109 msgstr "" 110 111 #. translators: %s: coupon code. 112 #: premium/Commerce/Woo/CartActionService.php:487 113 #, php-format 114 msgid "Code %s applied" 115 msgstr "" 116 117 #: premium/Commerce/Woo/CartActionService.php:488 118 msgid "Couldn't apply this code." 119 msgstr "" 120 121 #. translators: %s: minimum order amount with currency. 122 #: premium/Commerce/Woo/CartActionService.php:490 123 #, php-format 124 msgid "Minimum order %s" 125 msgstr "" 126 127 #: premium/Commerce/Woo/CartActionService.php:491 128 msgid "Discount codes" 129 msgstr "" 130 131 #: premium/CRM/CrmStore.php:609 132 msgid "Customer ID" 133 msgstr "" 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 139 msgid "Name" 140 msgstr "" 141 142 #: premium/CRM/CrmStore.php:611 143 msgid "Verified" 144 msgstr "" 145 146 #: premium/CRM/CrmStore.php:611 147 #: src/PHP/Widget/WidgetLoader.php:155 148 msgid "Yes" 149 msgstr "" 150 151 #: premium/CRM/CrmStore.php:611 152 msgid "No" 153 msgstr "" 154 155 #. translators: %s: contact type, for example "email" or "phone". 156 #: premium/CRM/CrmStore.php:616 157 #, php-format 158 msgid "Contact (%s)" 159 msgstr "" 160 161 #: premium/CRM/CrmStore.php:620 162 msgid "Ninhle AI Sales Assistant customer profile" 163 msgstr "" 164 165 #: premium/CRM/CrmStore.php:623 166 msgid "Ninhle AI Sales Assistant contact permissions" 167 msgstr "" 168 169 #: premium/CRM/CrmStore.php:624 170 msgid "Purpose" 171 msgstr "" 172 173 #: premium/CRM/CrmStore.php:625 174 #: premium/CRM/CrmStore.php:666 175 #: src/PHP/Privacy/PersonalData.php:296 176 msgid "Channel" 177 msgstr "" 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 183 msgid "Status" 184 msgstr "" 185 186 #: premium/CRM/CrmStore.php:627 187 msgid "Source" 188 msgstr "" 189 190 #: premium/CRM/CrmStore.php:628 191 msgid "Recorded" 192 msgstr "" 193 194 #: premium/CRM/CrmStore.php:634 195 msgid "Ninhle AI Sales Assistant purchase opportunities" 196 msgstr "" 197 198 #: premium/CRM/CrmStore.php:635 199 msgid "Need" 200 msgstr "" 201 202 #: premium/CRM/CrmStore.php:636 203 msgid "Stage" 204 msgstr "" 205 206 #: premium/CRM/CrmStore.php:637 207 msgid "Payment" 208 msgstr "" 209 210 #: premium/CRM/CrmStore.php:638 211 #: src/PHP/Widget/WidgetLoader.php:118 212 msgid "Products" 213 msgstr "" 214 215 #: premium/CRM/CrmStore.php:639 216 #: src/PHP/Admin/AdminMenu.php:215 217 msgid "Orders" 218 msgstr "" 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 224 msgid "Created" 225 msgstr "" 226 227 #: premium/CRM/CrmStore.php:664 228 msgid "Ninhle AI Sales Assistant customer history" 229 msgstr "" 230 231 #: premium/CRM/CrmStore.php:665 232 #: premium/CRM/CrmStore.php:675 233 msgid "Type" 234 msgstr "" 235 236 #: premium/CRM/CrmStore.php:667 237 msgid "Content" 238 msgstr "" 239 240 #: premium/CRM/CrmStore.php:668 241 #: src/PHP/Privacy/PersonalData.php:337 242 msgid "When" 243 msgstr "" 244 245 #: premium/CRM/CrmStore.php:674 246 msgid "Ninhle AI Sales Assistant support and follow-up requests" 247 msgstr "" 248 249 #: premium/CRM/CrmStore.php:677 250 msgid "Resolution" 251 msgstr "" 252 253 #. translators: %d: number of chats 254 #: premium/Insights/InsightsDigest.php:66 255 #, php-format 256 msgid "%d chat with the assistant in the last 7 days." 257 msgid_plural "%d chats with the assistant in the last 7 days." 258 msgstr[0] "" 259 msgstr[1] "" 260 261 #: premium/Insights/InsightsDigest.php:69 262 msgid "Questions the assistant could not answer:" 263 msgstr "" 264 265 #: premium/Insights/InsightsDigest.php:72 266 #: premium/Insights/InsightsDigest.php:94 267 msgid "None this week." 268 msgstr "" 269 270 #. translators: 1: customer question, 2: number of times asked 271 #: premium/Insights/InsightsDigest.php:76 272 #, php-format 273 msgid "- \"%1$s\" (%2$d×)" 274 msgstr "" 275 276 #: premium/Insights/InsightsDigest.php:80 277 msgid "What customers looked for but could not get:" 278 msgstr "" 279 280 #. translators: 1: product name, 2: number of times asked 281 #: premium/Insights/InsightsDigest.php:84 282 #, php-format 283 msgid "- %1$s: out of stock (%2$d×)" 284 msgstr "" 285 286 #. translators: 1: search words, 2: number of searches 287 #: premium/Insights/InsightsDigest.php:88 288 #, php-format 289 msgid "- \"%1$s\": no matching product (%2$d×)" 290 msgstr "" 291 292 #. translators: 1: reason, 2: percentage, 3: number of conversations labeled 293 #: premium/Insights/InsightsDigest.php:104 294 #, php-format 295 msgid "Most common reason shoppers did not buy: %1$s (%2$s%% of %3$d chats)." 296 msgstr "" 297 298 #. translators: %s: amount in US dollars 299 #: premium/Insights/InsightsDigest.php:110 300 #, php-format 301 msgid "AI cost in the last 7 days: %s" 302 msgstr "" 303 304 #. translators: %s: link to the Sales Insights tab 305 #: premium/Insights/InsightsDigest.php:113 306 #, php-format 307 msgid "Open Sales Insights: %s" 308 msgstr "" 309 310 #: premium/Insights/InsightsDigest.php:114 311 msgid "You can turn this email off in Sales Insights > Settings." 312 msgstr "" 313 314 #. translators: %s: site name 315 #: premium/Insights/InsightsDigest.php:118 316 #, php-format 317 msgid "[%s] Your weekly Sales Insights" 318 msgstr "" 319 320 #: premium/Insights/InsightsDigest.php:125 321 msgid "price" 322 msgstr "" 323 324 #: premium/Insights/InsightsDigest.php:126 325 msgid "shipping cost or time" 326 msgstr "" 327 328 #: premium/Insights/InsightsDigest.php:127 329 msgid "unsure about size or fit" 330 msgstr "" 331 332 #: premium/Insights/InsightsDigest.php:128 333 msgid "out of stock" 334 msgstr "" 335 336 #: premium/Insights/InsightsDigest.php:129 337 msgid "missing information" 338 msgstr "" 339 340 #: premium/Insights/InsightsDigest.php:130 341 msgid "waiting for a discount" 342 msgstr "" 343 344 #: premium/Insights/InsightsDigest.php:131 345 msgid "just browsing" 346 msgstr "" 347 348 #: premium/Insights/InsightsDigest.php:132 349 msgid "other" 350 msgstr "" 351 352 #: premium/Integrations/TelegramService.php:77 353 msgid "None" 354 msgstr "" 355 356 #. translators: %s: chat widget brand name. 357 #: premium/Integrations/TelegramService.php:79 358 #, php-format 359 msgid "New lead from %s" 360 msgstr "" 361 362 #: premium/Integrations/TelegramService.php:80 363 msgid "Name:" 364 msgstr "" 365 366 #: premium/Integrations/TelegramService.php:81 367 msgid "Email:" 368 msgstr "" 369 370 #: premium/Integrations/TelegramService.php:82 371 msgid "Phone:" 372 msgstr "" 373 374 #: premium/REST/TelegramController.php:55 375 #: src/PHP/Core/JobDispatcher.php:99 376 msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly." 377 msgstr "" 378 379 #: premium/Webhooks/WebhookDispatcher.php:113 380 msgid "Outgoing webhooks are available in the Pro plan." 381 msgstr "" 382 383 #: src/PHP/Abilities/AbilitiesService.php:57 384 msgid "Store catalog" 385 msgstr "" 386 387 #: src/PHP/Abilities/AbilitiesService.php:58 388 msgid "Read-only access to the public product catalog and store policies." 389 msgstr "" 390 391 #: src/PHP/Abilities/AbilitiesService.php:75 392 msgid "Search products" 393 msgstr "" 394 395 #: src/PHP/Abilities/AbilitiesService.php:76 396 msgid "Searches the public product catalog of this store. Returns up to 10 products with price, currency, stock status, link and image." 397 msgstr "" 398 399 #: src/PHP/Abilities/AbilitiesService.php:104 400 msgid "Get product details" 401 msgstr "" 402 403 #: src/PHP/Abilities/AbilitiesService.php:105 404 msgid "Returns public details of one product: price, stock, description, categories, attributes and, for products with options, each option with its price and stock." 405 msgstr "" 406 407 #: src/PHP/Abilities/AbilitiesService.php:120 408 msgid "Get store policies" 409 msgstr "" 410 411 #: src/PHP/Abilities/AbilitiesService.php:121 412 msgid "Returns the store delivery, refund and warranty policies and the payment methods it accepts." 413 msgstr "" 414 415 #: src/PHP/Abilities/AbilitiesService.php:153 416 msgid "Too many requests. Try again later." 417 msgstr "" 418 419 #: src/PHP/Abilities/AbilitiesService.php:197 420 msgid "Product not found." 421 msgstr "" 422 38 423 #: src/PHP/Admin/AdminMenu.php:26 39 424 msgid "Ninhle AI Sales Assistant" 40 425 msgstr "" 41 426 42 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:2743 427 #: src/PHP/Admin/AdminMenu.php:27 44 428 msgid "Ninhle AI Sales" 45 429 msgstr "" 46 430 47 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:9148 431 #: src/PHP/Admin/AdminMenu.php:91 432 msgid "Sales Insights" 433 msgstr "" 434 435 #: src/PHP/Admin/AdminMenu.php:92 436 msgid "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." 437 msgstr "" 438 439 #. translators: %s: number of chats 440 #: src/PHP/Admin/AdminMenu.php:94 441 #, php-format 442 msgid "%s chats in this period" 443 msgstr "" 444 445 #. translators: 1: number of chats so far, 2: number of chats needed 446 #: src/PHP/Admin/AdminMenu.php:96 447 #, php-format 448 msgid "Not enough data yet (%1$s of %2$s chats); the lists below are a first look." 449 msgstr "" 450 451 #: src/PHP/Admin/AdminMenu.php:98 452 msgid "Questions the assistant could not answer" 453 msgstr "" 454 455 #: src/PHP/Admin/AdminMenu.php:99 456 msgid "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." 457 msgstr "" 458 459 #: src/PHP/Admin/AdminMenu.php:100 460 msgid "No unanswered questions in this period." 461 msgstr "" 462 463 #: src/PHP/Admin/AdminMenu.php:101 464 #: src/PHP/Admin/AdminMenu.php:116 465 msgid "Question" 466 msgstr "" 467 468 #: src/PHP/Admin/AdminMenu.php:102 469 msgid "Times asked" 470 msgstr "" 471 472 #: src/PHP/Admin/AdminMenu.php:103 473 msgid "Last asked" 474 msgstr "" 475 476 #: src/PHP/Admin/AdminMenu.php:104 477 msgid "Actions" 478 msgstr "" 479 480 #. translators: %s: other ways the question was asked 481 #: src/PHP/Admin/AdminMenu.php:106 482 #, php-format 483 msgid "Also asked as: %s" 484 msgstr "" 485 486 #: src/PHP/Admin/AdminMenu.php:107 487 msgid "shopper said it was not answered" 488 msgstr "" 489 490 #: src/PHP/Admin/AdminMenu.php:108 491 msgid "information missing" 492 msgstr "" 493 494 #: src/PHP/Admin/AdminMenu.php:109 495 msgid "no matching product" 496 msgstr "" 497 498 #: src/PHP/Admin/AdminMenu.php:110 499 msgid "passed to your team" 500 msgstr "" 501 502 #: src/PHP/Admin/AdminMenu.php:111 503 msgid "local-only mode" 504 msgstr "" 505 506 #: src/PHP/Admin/AdminMenu.php:112 507 msgid "Add answer" 508 msgstr "" 509 510 #: src/PHP/Admin/AdminMenu.php:113 511 msgid "Dismiss" 512 msgstr "" 513 514 #: src/PHP/Admin/AdminMenu.php:114 515 msgid "Question dismissed." 516 msgstr "" 517 518 #: src/PHP/Admin/AdminMenu.php:115 519 msgid "Open a chat" 520 msgstr "" 521 522 #: src/PHP/Admin/AdminMenu.php:117 523 msgid "Answer" 524 msgstr "" 525 526 #: src/PHP/Admin/AdminMenu.php:118 527 msgid "Where to save the answer" 528 msgstr "" 529 530 #: src/PHP/Admin/AdminMenu.php:119 531 msgid "Add to Business information (the assistant uses it from the next reply)" 532 msgstr "" 533 534 #: src/PHP/Admin/AdminMenu.php:120 535 msgid "Save as a note so I can update my policy pages" 536 msgstr "" 537 538 #: src/PHP/Admin/AdminMenu.php:121 539 msgid "Save answer" 540 msgstr "" 541 542 #: src/PHP/Admin/AdminMenu.php:122 543 #: src/PHP/Widget/WidgetLoader.php:81 544 msgid "Cancel" 545 msgstr "" 546 547 #: src/PHP/Admin/AdminMenu.php:123 548 msgid "Answer added to Business information." 549 msgstr "" 550 551 #: src/PHP/Admin/AdminMenu.php:124 552 msgid "Answer saved as a note." 553 msgstr "" 554 555 #: src/PHP/Admin/AdminMenu.php:125 556 msgid "Could not save the answer. Enter both the question and the answer." 557 msgstr "" 558 559 #: src/PHP/Admin/AdminMenu.php:126 560 msgid "Notes for your policy pages" 561 msgstr "" 562 563 #: src/PHP/Admin/AdminMenu.php:127 564 msgid "Answers you chose to add to your own pages. The assistant does not read these notes." 565 msgstr "" 566 567 #: src/PHP/Admin/AdminMenu.php:128 568 msgid "Delete" 569 msgstr "" 570 571 #: src/PHP/Admin/AdminMenu.php:130 572 msgid "What shoppers could not find" 573 msgstr "" 574 575 #: src/PHP/Admin/AdminMenu.php:131 576 msgid "Searches with no matching product" 577 msgstr "" 578 579 #: src/PHP/Admin/AdminMenu.php:132 580 msgid "Wanted but out of stock" 581 msgstr "" 582 583 #: src/PHP/Admin/AdminMenu.php:133 584 msgid "None in this period." 585 msgstr "" 586 587 #: src/PHP/Admin/AdminMenu.php:134 588 msgid "Search your products" 589 msgstr "" 590 591 #: src/PHP/Admin/AdminMenu.php:135 592 msgid "In stock now" 593 msgstr "" 594 595 #: src/PHP/Admin/AdminMenu.php:136 596 msgid "Backorders allowed" 597 msgstr "" 598 599 #: src/PHP/Admin/AdminMenu.php:137 600 msgid "Restock it, or allow backorders under Product data > Inventory." 601 msgstr "" 602 603 #: src/PHP/Admin/AdminMenu.php:139 604 msgid "Why shoppers did not buy" 605 msgstr "" 606 607 #: src/PHP/Admin/AdminMenu.php:140 608 msgid "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." 609 msgstr "" 610 611 #: src/PHP/Admin/AdminMenu.php:141 612 msgid "Turn on AI labeling below to see why shoppers did not buy." 613 msgstr "" 614 615 #: src/PHP/Admin/AdminMenu.php:142 616 msgid "AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on." 617 msgstr "" 618 619 #. translators: 1: chats labeled so far, 2: chats needed 620 #: src/PHP/Admin/AdminMenu.php:144 621 #, php-format 622 msgid "Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough." 623 msgstr "" 624 625 #: src/PHP/Admin/AdminMenu.php:145 626 msgid "Price" 627 msgstr "" 628 629 #: src/PHP/Admin/AdminMenu.php:146 630 msgid "Shipping cost or time" 631 msgstr "" 632 633 #: src/PHP/Admin/AdminMenu.php:147 634 msgid "Unsure about size or fit" 635 msgstr "" 636 637 #: src/PHP/Admin/AdminMenu.php:148 638 #: src/PHP/Widget/WidgetLoader.php:123 639 msgid "Out of stock" 640 msgstr "" 641 642 #: src/PHP/Admin/AdminMenu.php:149 643 msgid "Missing information" 644 msgstr "" 645 646 #: src/PHP/Admin/AdminMenu.php:150 647 msgid "Waiting for a discount" 648 msgstr "" 649 650 #: src/PHP/Admin/AdminMenu.php:151 651 msgid "Just browsing" 652 msgstr "" 653 654 #: src/PHP/Admin/AdminMenu.php:152 655 msgid "Other" 656 msgstr "" 657 658 #: src/PHP/Admin/AdminMenu.php:153 659 msgid "See the unanswered questions" 660 msgstr "" 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 665 msgid "Model: %1$s · This month: %2$s of %3$s" 666 msgstr "" 667 668 #. translators: %s: date 669 #: src/PHP/Admin/AdminMenu.php:157 670 #, php-format 671 msgid "Last run: %s" 672 msgstr "" 673 674 #: src/PHP/Admin/AdminMenu.php:158 675 msgid "Stopped: the monthly limit for AI labeling was reached." 676 msgstr "" 677 678 #: src/PHP/Admin/AdminMenu.php:159 679 msgid "Stopped: the overall AI budget was reached." 680 msgstr "" 681 682 #: src/PHP/Admin/AdminMenu.php:160 683 msgid "Stopped: no AI provider with an API key is set up." 684 msgstr "" 685 686 #: src/PHP/Admin/AdminMenu.php:161 687 msgid "Paused: 200 chats are labeled per night at most." 688 msgstr "" 689 690 #: src/PHP/Admin/AdminMenu.php:163 691 msgid "Sales Insights settings" 692 msgstr "" 693 694 #: src/PHP/Admin/AdminMenu.php:164 695 msgid "Label why shoppers did not buy, each night with AI" 696 msgstr "" 697 698 #: src/PHP/Admin/AdminMenu.php:165 699 msgid "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." 700 msgstr "" 701 702 #: src/PHP/Admin/AdminMenu.php:166 703 msgid "Monthly limit (USD)" 704 msgstr "" 705 706 #: src/PHP/Admin/AdminMenu.php:167 707 msgid "Model" 708 msgstr "" 709 710 #: src/PHP/Admin/AdminMenu.php:168 711 msgid "Cheapest available (recommended)" 712 msgstr "" 713 714 #: src/PHP/Admin/AdminMenu.php:169 715 msgid "Same as the chat assistant" 716 msgstr "" 717 718 #: src/PHP/Admin/AdminMenu.php:170 719 msgid "Email me a weekly summary" 720 msgstr "" 721 722 #: src/PHP/Admin/AdminMenu.php:171 723 msgid "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." 724 msgstr "" 725 726 #: src/PHP/Admin/AdminMenu.php:178 49 727 msgid "AI Value & Performance" 50 728 msgstr "" 51 729 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 54 731 msgid "What the assistant cost, the orders it helped with and how conversations ended. Figures start on the measurement start date and are never backfilled." 55 732 msgstr "" 56 733 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 59 735 msgid "Report period" 60 736 msgstr "" 61 737 62 738 #. 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 65 740 #, php-format 66 741 msgid "Last %s days" … … 68 743 69 744 #. 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 72 746 #, php-format 73 747 msgid "Measured since %s" 74 748 msgstr "" 75 749 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 78 751 msgid "Measurement starts after the database update to version 2.4.0." 79 752 msgstr "" 80 753 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 83 755 msgid "Loading the figures…" 84 756 msgstr "" 85 757 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 88 759 msgid "Could not load the figures. Please reload the page." 89 760 msgstr "" 90 761 91 762 #. 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 94 764 #, php-format 95 765 msgid "n = %s" 96 766 msgstr "" 97 767 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 100 769 msgid "Cost and estimated comparison" 101 770 msgstr "" 102 771 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 105 773 msgid "AI cost recorded" 106 774 msgstr "" 107 775 108 776 #. 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 111 778 #, php-format 112 779 msgid "%1$s from provider usage, %2$s estimated" … … 114 781 115 782 #. 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 118 784 #, php-format 119 785 msgid "%s requests used a model with no known price and are counted as $0." 120 786 msgstr "" 121 787 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 124 789 msgid "Plugin cost, allocated per day" 125 790 msgstr "" 126 791 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 129 793 msgid "Other costs you entered" 130 794 msgstr "" 131 795 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 134 797 msgid "Total" 135 798 msgstr "" 136 799 137 800 #. 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 140 802 #, php-format 141 803 msgid "Reference option: %s" … … 143 805 144 806 #. 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 147 808 #, php-format 148 809 msgid "%1$s × %2$s" 149 810 msgstr "" 150 811 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 153 813 msgid "Estimated difference" 154 814 msgstr "" 155 815 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 158 817 msgid "Estimate based on the reference option you chose; not a verified saving." 159 818 msgstr "" 160 819 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 163 821 msgid "Add a reference option in Measurement settings to compare costs." 164 822 msgstr "" 165 823 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 168 825 msgid "AI cost covers model usage only. It is not the total cost of ownership." 169 826 msgstr "" 170 827 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 173 829 msgid "per AI conversation" 174 830 msgstr "" 175 831 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 178 833 msgid "per conversation the customer confirmed as resolved" 179 834 msgstr "" 180 835 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 183 837 msgid "per month" 184 838 msgstr "" 185 839 186 840 #. 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 189 842 #, php-format 190 843 msgid "Source: %s" 191 844 msgstr "" 192 845 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 195 847 msgid "AI-assisted orders" 196 848 msgstr "" 197 849 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 206 851 msgid "AI-assisted does not mean the AI was the only reason for the order." 207 852 msgstr "" 208 853 209 854 #. 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 212 856 #, php-format 213 857 msgid "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." 214 858 msgstr "" 215 859 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 218 861 msgid "AI-assisted revenue" 219 862 msgstr "" 220 863 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 223 865 msgid "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." 224 866 msgstr "" 225 867 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 228 869 msgid "Revenue per currency is included in the Starter and Pro plans." 229 870 msgstr "" 230 871 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 233 873 msgid "No AI-assisted revenue in this period." 234 874 msgstr "" 235 875 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 238 877 msgid "Conversation outcomes" 239 878 msgstr "" 240 879 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 243 881 msgid "Finished conversations" 244 882 msgstr "" 245 883 246 884 #. 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 249 886 #, php-format 250 887 msgid "Not enough data yet: %1$s of %2$s finished conversations needed before rates are shown." 251 888 msgstr "" 252 889 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 255 891 msgid "Resolved, confirmed by the customer" 256 892 msgstr "" 257 893 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 260 895 msgid "Probably resolved: the customer clicked a product, added to cart or ordered" 261 896 msgstr "" 262 897 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 265 899 msgid "Handed off to staff" 266 900 msgstr "" 267 901 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 270 903 msgid "Unresolved, left before an answer, or unknown" 271 904 msgstr "" 272 905 273 906 #. 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 276 908 #, php-format 277 909 msgid "95%% range: %1$s–%2$s%%" 278 910 msgstr "" 279 911 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 282 913 msgid "\"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." 283 914 msgstr "" 284 915 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 287 917 msgid "Handoffs by source" 288 918 msgstr "" 289 919 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 292 921 msgid "AI" 293 922 msgstr "" 294 923 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 297 925 msgid "Automation rule" 298 926 msgstr "" 299 927 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 302 929 msgid "Customer button" 303 930 msgstr "" 304 931 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 307 933 msgid "Staff took over" 308 934 msgstr "" 309 935 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 312 937 msgid "AI budget reached" 313 938 msgstr "" 314 939 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 317 941 msgid "Measurement settings" 318 942 msgstr "" 319 943 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 322 945 msgid "Reference option name" 323 946 msgstr "" 324 947 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 327 949 msgid "Pricing unit" 328 950 msgstr "" 329 951 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 332 953 msgid "Price (USD)" 333 954 msgstr "" 334 955 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 337 957 msgid "Where the price comes from" 338 958 msgstr "" 339 959 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 342 961 msgid "Price date" 343 962 msgstr "" 344 963 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 347 965 msgid "Enter the price of an alternative you are considering. Leave the price at 0 to show costs only." 348 966 msgstr "" 349 967 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 352 969 msgid "Plugin cost per month (USD)" 353 970 msgstr "" 354 971 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 357 973 msgid "Other costs per month (USD)" 358 974 msgstr "" 359 975 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 362 977 msgid "Attribution window" 363 978 msgstr "" 364 979 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 367 981 msgid "How long after a qualifying chat interaction an order still counts as AI-assisted." 368 982 msgstr "" 369 983 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 372 985 msgid "24 hours" 373 986 msgstr "" 374 987 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 377 989 msgid "72 hours" 378 990 msgstr "" 379 991 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 382 993 msgid "7 days" 383 994 msgstr "" 384 995 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 387 997 msgid "Order statuses that count" 388 998 msgstr "" 389 999 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 392 1001 msgid "Processing" 393 1002 msgstr "" 394 1003 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 397 1005 msgid "Completed" 398 1006 msgstr "" 399 1007 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 402 1009 msgid "On hold" 403 1010 msgstr "" 404 1011 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 407 1013 msgid "Save settings" 408 1014 msgstr "" 409 1015 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 412 1017 msgid "Saving…" 413 1018 msgstr "" 414 1019 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 417 1021 msgid "Measurement settings saved." 418 1022 msgstr "" 419 1023 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 422 1025 msgid "Could not save the measurement settings." 423 1026 msgstr "" 424 1027 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 1031 msgid "In the last 90 days, your AI sales assistant answered %s shopper conversation." 1032 msgid_plural "In the last 90 days, your AI sales assistant answered %s shopper conversations." 1033 msgstr[0] "" 1034 msgstr[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 1039 msgid "%s order was placed after a chat with it." 1040 msgid_plural "%s orders were placed after a chat with it." 1041 msgstr[0] "" 1042 msgstr[1] "" 1043 1044 #: src/PHP/Admin/ReviewPrompt.php:192 1045 msgid "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." 1046 msgstr "" 1047 1048 #: src/PHP/Admin/ReviewPrompt.php:194 1049 msgid "Rate on WordPress.org" 1050 msgstr "" 1051 1052 #: src/PHP/Admin/ReviewPrompt.php:196 1053 msgid "Maybe later" 1054 msgstr "" 1055 1056 #: src/PHP/Admin/ReviewPrompt.php:198 1057 msgid "Don't ask again" 1058 msgstr "" 1059 1060 #: src/PHP/Admin/ReviewPrompt.php:200 1061 msgid "Need help? Ask on the support forum" 1062 msgstr "" 1063 426 1064 #: src/PHP/Admin/SecurityNotice.php:94 427 1065 msgid "Ninhle AI Sales Assistant: rotate your API credentials" 428 1066 msgstr "" 429 1067 430 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:95431 1068 #: src/PHP/Admin/SecurityNotice.php:95 432 1069 msgid "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." 433 1070 msgstr "" 434 1071 435 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:99436 1072 #: src/PHP/Admin/SecurityNotice.php:99 437 1073 msgid "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." 438 1074 msgstr "" 439 1075 440 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:104441 1076 #: src/PHP/Admin/SecurityNotice.php:104 442 1077 msgid "Dismiss this notice" 443 1078 msgstr "" 444 1079 445 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:16446 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:17447 1080 #: src/PHP/Admin/SettingsPage.php:16 448 1081 #: src/PHP/Admin/SettingsPage.php:17 … … 451 1084 452 1085 #. translators: %s: commission rate, for example "30%" 453 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:45454 1086 #: src/PHP/Admin/SettingsPage.php:45 455 1087 #, php-format … … 457 1089 msgstr "" 458 1090 459 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:68460 1091 #: src/PHP/AI/AIService.php:68 461 1092 msgid "" … … 465 1096 msgstr "" 466 1097 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 469 1099 msgid "The assistant is running in local-only mode. Please contact us through the channels below for help." 470 1100 msgstr "" 471 1101 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 474 1103 msgid "Hello! The AI assistant is still being set up. Please try again shortly, or contact us through the channels below." 475 1104 msgstr "" 476 1105 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 479 1107 msgid "Sorry, the assistant is temporarily unavailable. Please try again in a moment, or choose \"Talk to a person\"." 480 1108 msgstr "" 481 1109 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 1111 msgid "Sorry, I could not finish looking that up. Please ask me again in a moment." 1112 msgstr "" 1113 1114 #: src/PHP/AI/AIService.php:461 489 1115 msgid "Sorry, I could not put an answer together just now. Could you ask that again in a different way?" 490 1116 msgstr "" 491 1117 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 494 1119 msgid "I understand. Let me bring in a member of our team to help you directly. Please wait a moment." 495 1120 msgstr "" 496 1121 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 499 1123 msgid "We are connecting you with a manager now. Please stay in the chat." 500 1124 msgstr "" 501 1125 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 504 1127 msgid "Your request is being escalated urgently to the responsible team. Please do not send any more personal information here." 505 1128 msgstr "" 506 1129 507 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:69508 1130 #: src/PHP/Commerce/LocalHandler.php:69 509 1131 msgid "Hello! How can I help you today?" … … 511 1133 512 1134 #. translators: %s is the store or brand name. 513 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:74514 1135 #: src/PHP/Commerce/LocalHandler.php:74 515 1136 #, php-format … … 517 1138 msgstr "" 518 1139 519 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:80520 1140 #: src/PHP/Commerce/LocalHandler.php:80 521 1141 msgid "You are welcome. Just message me if you need anything else." 522 1142 msgstr "" 523 1143 524 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:123525 1144 #: src/PHP/Commerce/LocalHandler.php:123 526 1145 msgid "Your cart is empty. Tell me which product you want and I will send you the link." … … 528 1147 529 1148 #. translators: %s is the checkout page URL. 530 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:135531 1149 #: src/PHP/Commerce/LocalHandler.php:135 532 1150 #, php-format … … 535 1153 536 1154 #. translators: %s is the cart page URL. 537 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:155538 1155 #: src/PHP/Commerce/LocalHandler.php:155 539 1156 #, php-format … … 541 1158 msgstr "" 542 1159 543 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:163544 1160 #: src/PHP/Commerce/LocalHandler.php:163 1161 #: src/PHP/Widget/WidgetLoader.php:142 545 1162 msgid "Your cart is empty." 546 1163 msgstr "" 547 1164 548 1165 #. translators: 1: list of cart lines, one per line. 2: checkout URL. 549 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:173550 1166 #: src/PHP/Commerce/LocalHandler.php:173 551 1167 #, php-format … … 558 1174 559 1175 #. 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 562 1177 #, php-format 563 1178 msgid "Order %1$s is currently: %2$s (placed on %3$s)." 564 1179 msgstr "" 565 1180 566 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:197567 1181 #: src/PHP/Commerce/Woo/LeadService.php:197 568 1182 msgid "Human support needed" 569 1183 msgstr "" 570 1184 571 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:198572 1185 #: src/PHP/Commerce/Woo/LeadService.php:198 573 1186 msgid "Priority:" 574 1187 msgstr "" 575 1188 576 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:199577 1189 #: src/PHP/Commerce/Woo/LeadService.php:199 578 1190 msgid "Issue:" 579 1191 msgstr "" 580 1192 581 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:200582 1193 #: src/PHP/Commerce/Woo/LeadService.php:200 583 1194 msgid "Summary:" … … 585 1196 586 1197 #. translators: %d: conversation ID. 587 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:202588 1198 #: src/PHP/Commerce/Woo/LeadService.php:202 589 1199 #, php-format … … 591 1201 msgstr "" 592 1202 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 1204 msgid "Ninhle AI Sales Assistant: background replies are blocked by your host" 1205 msgstr "" 1206 1207 #: src/PHP/Core/AsyncKick.php:343 1208 msgid "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." 1209 msgstr "" 1210 1211 #: src/PHP/Core/AsyncKick.php:344 1212 msgid "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." 1213 msgstr "" 1214 601 1215 #: src/PHP/Privacy/PersonalData.php:35 602 1216 #: src/PHP/Privacy/PersonalData.php:43 … … 604 1218 msgstr "" 605 1219 606 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:144607 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:206608 1220 #: src/PHP/Privacy/PersonalData.php:144 609 #: src/PHP/Privacy/PersonalData.php:2 061221 #: src/PHP/Privacy/PersonalData.php:229 610 1222 msgid "Conversation" 611 1223 msgstr "" 612 1224 613 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:145614 1225 #: src/PHP/Privacy/PersonalData.php:145 615 1226 msgid "Sender" 616 1227 msgstr "" 617 1228 618 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:146619 1229 #: src/PHP/Privacy/PersonalData.php:146 620 1230 msgid "Message" 621 1231 msgstr "" 622 1232 623 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:147624 1233 #: src/PHP/Privacy/PersonalData.php:147 625 1234 msgid "Sent" 626 1235 msgstr "" 627 1236 628 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:153629 1237 #: src/PHP/Privacy/PersonalData.php:153 630 1238 msgid "Sender first name" 631 1239 msgstr "" 632 1240 633 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:154634 1241 #: src/PHP/Privacy/PersonalData.php:154 635 1242 msgid "Sender last name" 636 1243 msgstr "" 637 1244 638 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:155639 1245 #: src/PHP/Privacy/PersonalData.php:155 640 1246 msgid "Sender avatar" 641 1247 msgstr "" 642 1248 643 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:164644 1249 #: src/PHP/Privacy/PersonalData.php:164 645 1250 msgid "Ninhle AI Sales Assistant chat messages" 646 1251 msgstr "" 647 1252 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 1254 msgid "Order linked to this chat" 1255 msgstr "" 1256 1257 #: src/PHP/Privacy/PersonalData.php:208 1258 msgid "Question the assistant could not answer (contact details hidden)" 1259 msgstr "" 1260 1261 #: src/PHP/Privacy/PersonalData.php:211 1262 msgid "Customer identified in this chat" 1263 msgstr "" 1264 1265 #. translators: %d: WordPress user ID of the logged-in customer. 1266 #: src/PHP/Privacy/PersonalData.php:213 1267 #, php-format 1268 msgid "Logged-in account #%d" 1269 msgstr "" 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 1274 msgid "Order ID %d (ownership checked)" 1275 msgstr "" 1276 1277 #: src/PHP/Privacy/PersonalData.php:218 1278 msgid "Reason the chat did not lead to an order (AI label)" 1279 msgstr "" 1280 1281 #: src/PHP/Privacy/PersonalData.php:222 1282 msgid "Answer to \"Did this answer your question?\"" 1283 msgstr "" 1284 1285 #: src/PHP/Privacy/PersonalData.php:226 650 1286 msgid "Ninhle AI Sales Assistant chat statistics" 651 1287 msgstr "" 652 1288 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 665 1290 msgid "Recorded (UTC)" 666 1291 msgstr "" 667 1292 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 682 1294 #: src/PHP/REST/LeadController.php:146 683 #: src/PHP/Widget/WidgetLoader.php:9 01295 #: src/PHP/Widget/WidgetLoader.php:95 684 1296 msgid "Email" 685 1297 msgstr "" 686 1298 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 691 1300 #: src/PHP/REST/LeadController.php:147 692 #: src/PHP/Widget/WidgetLoader.php:9 11301 #: src/PHP/Widget/WidgetLoader.php:96 693 1302 msgid "Phone" 694 1303 msgstr "" 695 1304 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 707 1306 msgid "Lead score" 708 1307 msgstr "" 709 1308 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 721 1310 msgid "Additional fields" 722 1311 msgstr "" 723 1312 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 726 1314 msgid "Ninhle AI Sales Assistant contact details" 727 1315 msgstr "" 728 1316 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 731 1318 msgid "Ninhle AI Sales Assistant chat sessions" 732 1319 msgstr "" 733 1320 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 743 1322 msgid "Session reference" 744 1323 msgstr "" 745 1324 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 748 1326 msgid "First seen" 749 1327 msgstr "" 750 1328 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 753 1330 msgid "Ninhle AI Sales Assistant activity log" 754 1331 msgstr "" 755 1332 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 758 1334 msgid "Action" 759 1335 msgstr "" 760 1336 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 763 1338 msgid "IP address" 764 1339 msgstr "" 765 1340 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 768 1342 msgid "Browser" 769 1343 msgstr "" 770 1344 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 779 1346 msgid "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." 780 1347 msgstr "" 781 1348 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 784 1350 msgid "This person was linked to records in a connected CRM. Copies already synced there must be erased in that CRM." 785 1351 msgstr "" 786 1352 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 789 1354 msgid "Customer records in Ninhle AI Sales Assistant CRM could not be erased because of a database error. Please run the erasure again." 790 1355 msgstr "" 791 1356 792 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:42793 1357 #: src/PHP/Privacy/PolicyContent.php:42 794 1358 msgid "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." 795 1359 msgstr "" 796 1360 797 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:46798 1361 #: src/PHP/Privacy/PolicyContent.php:46 799 1362 msgid "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." 800 1363 msgstr "" 801 1364 802 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:49803 1365 #: src/PHP/Privacy/PolicyContent.php:49 804 1366 msgid "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." 805 1367 msgstr "" 806 1368 807 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:53808 1369 #: src/PHP/Privacy/PolicyContent.php:53 809 1370 msgid "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." 810 1371 msgstr "" 811 1372 812 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:55813 1373 #: src/PHP/Privacy/PolicyContent.php:55 814 1374 msgid "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." … … 816 1376 817 1377 #. translators: %s is the name of the configured AI provider. 818 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:59819 1378 #: src/PHP/Privacy/PolicyContent.php:59 820 1379 #, php-format … … 822 1381 msgstr "" 823 1382 824 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:64825 1383 #: src/PHP/Privacy/PolicyContent.php:64 826 1384 msgid "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." 827 1385 msgstr "" 828 1386 829 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:68830 1387 #: src/PHP/Privacy/PolicyContent.php:68 831 1388 msgid "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." 832 1389 msgstr "" 833 1390 834 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:73835 1391 #: src/PHP/Privacy/PolicyContent.php:73 836 1392 msgid "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." 837 1393 msgstr "" 838 1394 839 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:77840 1395 #: src/PHP/Privacy/PolicyContent.php:77 841 1396 msgid "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." … … 843 1398 844 1399 #. translators: %d is the number of days files are kept. 845 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:84846 1400 #: src/PHP/Privacy/PolicyContent.php:84 847 1401 #, php-format … … 851 1405 msgstr[1] "" 852 1406 853 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:94854 1407 #: src/PHP/Privacy/PolicyContent.php:94 855 1408 msgid "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." 856 1409 msgstr "" 857 1410 858 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:99859 1411 #: src/PHP/Privacy/PolicyContent.php:99 860 1412 msgid "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." 861 1413 msgstr "" 862 1414 863 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:109864 1415 #: src/PHP/Privacy/PolicyContent.php:109 865 1416 msgid "OpenAI (see openai.com/policies/privacy-policy)" 866 1417 msgstr "" 867 1418 868 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:110869 1419 #: src/PHP/Privacy/PolicyContent.php:110 870 1420 msgid "Anthropic (see anthropic.com/legal/privacy)" 871 1421 msgstr "" 872 1422 873 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:111874 1423 #: src/PHP/Privacy/PolicyContent.php:111 875 1424 msgid "Google (see policies.google.com/privacy)" 876 1425 msgstr "" 877 1426 878 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:114879 1427 #: src/PHP/Privacy/PolicyContent.php:114 880 1428 msgid "the AI provider configured by the store owner" 881 1429 msgstr "" 882 1430 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 885 1432 msgid "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)." 886 1433 msgstr "" 887 1434 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 890 1436 msgid "Sales Assistant" 891 1437 msgstr "" 892 1438 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 895 1440 msgid "Thanks, our team has been notified. A member of staff will reply here as soon as possible." 896 1441 msgstr "" 897 1442 898 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:98899 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:142900 1443 #: src/PHP/REST/Guard.php:98 901 1444 #: src/PHP/REST/Guard.php:142 … … 903 1446 msgstr "" 904 1447 905 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:124906 1448 #: src/PHP/REST/Guard.php:124 907 1449 msgid "Security check failed. Please reload the page and try again." 908 1450 msgstr "" 909 1451 910 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:165911 1452 #: src/PHP/REST/Guard.php:165 912 1453 msgid "Missing chat session." 913 1454 msgstr "" 914 1455 915 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:175916 1456 #: src/PHP/REST/Guard.php:175 917 1457 msgid "Invalid or expired chat session." 918 1458 msgstr "" 919 1459 920 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:184921 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:199922 1460 #: src/PHP/REST/Guard.php:184 923 1461 #: src/PHP/REST/Guard.php:199 … … 925 1463 msgstr "" 926 1464 927 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:215928 1465 #: src/PHP/REST/Guard.php:215 929 1466 msgid "Too many requests. Please wait a moment." 930 1467 msgstr "" 931 1468 932 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:242933 1469 #: src/PHP/REST/Guard.php:242 934 1470 msgid "Telegram webhook secret is not configured." 935 1471 msgstr "" 936 1472 937 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:255938 1473 #: src/PHP/REST/Guard.php:255 939 1474 msgid "Invalid webhook signature." 940 1475 msgstr "" 941 1476 942 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:308943 1477 #: src/PHP/REST/Guard.php:308 944 1478 msgid "Message must be text." 945 1479 msgstr "" 946 1480 947 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:313948 1481 #: src/PHP/REST/Guard.php:313 949 1482 msgid "Message contains invalid characters." 950 1483 msgstr "" 951 1484 952 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:317953 1485 #: src/PHP/REST/Guard.php:317 954 1486 msgid "Message cannot be empty." 955 1487 msgstr "" 956 1488 957 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:321958 1489 #: src/PHP/REST/Guard.php:321 959 1490 msgid "Message is too large." 960 1491 msgstr "" 961 1492 962 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:325963 1493 #: src/PHP/REST/Guard.php:325 964 1494 msgid "Message is too long." 965 1495 msgstr "" 966 1496 967 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:149 1497 #: src/PHP/REST/InsightsController.php:43 1498 msgid "Enter both the question and the answer." 1499 msgstr "" 1500 1501 #: src/PHP/REST/InsightsController.php:66 1502 msgid "AI insights need the Starter plan." 1503 msgstr "" 1504 968 1505 #: src/PHP/REST/LeadController.php:149 969 1506 msgid "Score" 970 1507 msgstr "" 971 1508 972 #: build/ninhle-ai-sales-assistant/src/PHP/Rules/RuleEngine.php:232973 1509 #: src/PHP/Rules/RuleEngine.php:232 974 1510 msgid "A member of our team will join this chat shortly." 975 1511 msgstr "" 976 1512 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 979 1514 msgid "Open chat" 980 1515 msgstr "" 981 1516 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 1520 msgid "new replies: %d" 1521 msgstr "" 1522 1523 #: src/PHP/Widget/WidgetLoader.php:74 984 1524 msgid "Chat with our assistant" 985 1525 msgstr "" 986 1526 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 989 1528 msgid "Cart" 990 1529 msgstr "" 991 1530 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 994 1532 msgid "My details" 995 1533 msgstr "" 996 1534 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 999 1536 msgid "Clear conversation" 1000 1537 msgstr "" 1001 1538 1002 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:731003 #: src/PHP/Widget/WidgetLoader.php: 731539 #: src/PHP/Widget/WidgetLoader.php:78 1540 #: src/PHP/Widget/WidgetLoader.php:139 1004 1541 msgid "Close" 1005 1542 msgstr "" 1006 1543 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 1009 1545 msgid "Clear the conversation history and start over?" 1010 1546 msgstr "" 1011 1547 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 1014 1549 msgid "Clear" 1015 1550 msgstr "" 1016 1551 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 1024 1553 msgid "Attach a photo or PDF" 1025 1554 msgstr "" 1026 1555 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 1029 1557 msgid "Uploading…" 1030 1558 msgstr "" 1031 1559 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 1034 1561 msgid "File upload failed." 1035 1562 msgstr "" 1036 1563 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 1039 1565 msgid "Your message could not be sent. Please try again." 1040 1566 msgstr "" 1041 1567 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 1044 1569 msgid "Call us" 1045 1570 msgstr "" 1046 1571 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 1049 1573 msgid "Send" 1050 1574 msgstr "" 1051 1575 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 1054 1577 msgid "Your message" 1055 1578 msgstr "" 1056 1579 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 1059 1581 msgid "Type your question…" 1060 1582 msgstr "" 1061 1583 1062 1584 #. 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 1065 1586 #, php-format 1066 1587 msgid "File is too large. Please choose a file under %sMB." 1067 1588 msgstr "" 1068 1589 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 1071 1591 msgid "Thank you. Our team will contact you shortly." 1072 1592 msgstr "" 1073 1593 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 1076 1595 msgid "Your contact details" 1077 1596 msgstr "" 1078 1597 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 1081 1599 msgid "Submit" 1082 1600 msgstr "" 1083 1601 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 1086 1603 msgid "Submitting…" 1087 1604 msgstr "" 1088 1605 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 1091 1607 msgid "Please provide at least an email or phone number." 1092 1608 msgstr "" 1093 1609 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 1096 1611 msgid "Could not submit your details." 1097 1612 msgstr "" 1098 1613 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 1101 1615 msgid "More options" 1102 1616 msgstr "" 1103 1617 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 1106 1619 msgid "Expand" 1107 1620 msgstr "" 1108 1621 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 1111 1623 msgid "Make smaller" 1112 1624 msgstr "" 1113 1625 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 1116 1627 msgid "Talk to a person" 1117 1628 msgstr "" 1118 1629 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 1121 1631 msgid "AI assistant" 1122 1632 msgstr "" 1123 1633 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 1126 1635 msgid "Waiting for a team member" 1127 1636 msgstr "" 1128 1637 1129 1638 #. 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 1132 1640 #, php-format 1133 1641 msgid "%s is helping you" 1134 1642 msgstr "" 1135 1643 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 1138 1645 msgid "A team member is helping you" 1139 1646 msgstr "" 1140 1647 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 1143 1649 msgid "This conversation is closed" 1144 1650 msgstr "" 1145 1651 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 1148 1653 msgid "Writing a reply…" 1149 1654 msgstr "" 1150 1655 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 1153 1657 msgid "Checking product information…" 1154 1658 msgstr "" 1155 1659 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 1158 1661 msgid "Looking up your order…" 1159 1662 msgstr "" 1160 1663 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 1163 1665 msgid "Connecting you with a team member…" 1164 1666 msgstr "" 1165 1667 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 1669 msgid "Waiting in line…" 1670 msgstr "" 1671 1672 #: src/PHP/Widget/WidgetLoader.php:119 1673 msgid "Previous products" 1674 msgstr "" 1675 1676 #: src/PHP/Widget/WidgetLoader.php:120 1677 msgid "Next products" 1678 msgstr "" 1679 1680 #: src/PHP/Widget/WidgetLoader.php:121 1681 msgid "View" 1682 msgstr "" 1683 1684 #: src/PHP/Widget/WidgetLoader.php:122 1685 msgid "In stock" 1686 msgstr "" 1687 1688 #: src/PHP/Widget/WidgetLoader.php:124 1689 msgid "On backorder" 1690 msgstr "" 1691 1692 #. translators: %s: date the price was shown, e.g. "24 Oct 2026". 1693 #: src/PHP/Widget/WidgetLoader.php:126 1694 #, php-format 1695 msgid "Price on %s" 1696 msgstr "" 1697 1698 #: src/PHP/Widget/WidgetLoader.php:128 1699 msgid "Add to cart" 1700 msgstr "" 1701 1702 #: src/PHP/Widget/WidgetLoader.php:129 1703 msgid "Adding…" 1704 msgstr "" 1705 1706 #: src/PHP/Widget/WidgetLoader.php:130 1707 msgid "Choose options" 1708 msgstr "" 1709 1710 #: src/PHP/Widget/WidgetLoader.php:131 1711 msgid "Added to cart" 1712 msgstr "" 1713 1714 #: src/PHP/Widget/WidgetLoader.php:132 1715 msgid "View cart" 1716 msgstr "" 1717 1718 #: src/PHP/Widget/WidgetLoader.php:133 1719 msgid "Checkout" 1720 msgstr "" 1721 1722 #: src/PHP/Widget/WidgetLoader.php:134 1723 msgid "Sorry, this item just sold out." 1724 msgstr "" 1725 1726 #: src/PHP/Widget/WidgetLoader.php:135 1727 msgid "You can't add more of this item." 1728 msgstr "" 1729 1730 #: src/PHP/Widget/WidgetLoader.php:136 1731 msgid "Couldn't add to cart." 1732 msgstr "" 1733 1734 #: src/PHP/Widget/WidgetLoader.php:137 1735 msgid "Open the product page" 1736 msgstr "" 1737 1738 #: src/PHP/Widget/WidgetLoader.php:138 1739 msgid "Your cart" 1740 msgstr "" 1741 1742 #: src/PHP/Widget/WidgetLoader.php:140 1743 msgid "Loading your cart…" 1744 msgstr "" 1745 1746 #: src/PHP/Widget/WidgetLoader.php:141 1747 msgid "Couldn't load your cart." 1748 msgstr "" 1749 1750 #: src/PHP/Widget/WidgetLoader.php:143 1751 msgid "Subtotal" 1752 msgstr "" 1753 1754 #: src/PHP/Widget/WidgetLoader.php:144 1755 msgid "Discount" 1756 msgstr "" 1757 1758 #: src/PHP/Widget/WidgetLoader.php:146 1759 msgid "Your orders" 1760 msgstr "" 1761 1762 #. translators: %s: order number. 1763 #: src/PHP/Widget/WidgetLoader.php:148 1764 #, php-format 1765 msgid "Order #%s" 1766 msgstr "" 1767 1768 #. translators: %d: number of other items in the order. 1769 #: src/PHP/Widget/WidgetLoader.php:150 1770 #, php-format 1771 msgid "and %d more" 1772 msgstr "" 1773 1774 #: src/PHP/Widget/WidgetLoader.php:151 1775 msgid "Tracking number" 1776 msgstr "" 1777 1778 #: src/PHP/Widget/WidgetLoader.php:152 1779 msgid "Track" 1780 msgstr "" 1781 1782 #: src/PHP/Widget/WidgetLoader.php:153 1783 msgid "View order" 1784 msgstr "" 1785 1786 #: src/PHP/Widget/WidgetLoader.php:154 1168 1787 msgid "Did this answer your question?" 1169 1788 msgstr "" 1170 1789 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 1179 1791 msgid "Not yet" 1180 1792 msgstr "" 1181 1793 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 1184 1795 msgid "Skip" 1185 1796 msgstr "" 1186 1797 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 1189 1799 msgid "Thanks for letting us know." 1190 1800 msgstr "" 1191 1801 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 1194 1803 msgid "Other ways to reach us" 1195 1804 msgstr "" 1196 1805 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 1199 1807 msgid "Speak your message" 1200 1808 msgstr "" 1201 1809 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 1204 1811 msgid "Stop listening" 1205 1812 msgstr "" 1206 1813 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 1209 1815 msgid "Listening…" 1210 1816 msgstr "" 1211 1817 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 1214 1819 msgid "Microphone access is blocked. Allow it in your browser settings, or type your message." 1215 1820 msgstr "" 1216 1821 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 1219 1823 msgid "No speech was heard. Please try again." 1220 1824 msgstr "" 1221 1825 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 1224 1827 msgid "No microphone was found. Please type your message." 1225 1828 msgstr "" 1226 1829 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 1229 1831 msgid "Voice input needs an internet connection. Please type your message." 1230 1832 msgstr "" 1231 1833 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 1234 1835 msgid "Your browser cannot recognise this language. Please type your message." 1235 1836 msgstr "" 1236 1837 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 1239 1839 msgid "Voice input is not available right now. Please type your message." 1240 1840 msgstr "" 1241 1841 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 1244 1843 msgid "You are chatting with an AI assistant. A team member can join when needed." 1245 1844 msgstr "" 1246 1247 #: premium/CRM/CrmStore.php:6091248 msgid "Customer ID"1249 msgstr ""1250 1251 #: premium/CRM/CrmStore.php:6111252 msgid "Verified"1253 msgstr ""1254 1255 #: premium/CRM/CrmStore.php:6111256 msgid "No"1257 msgstr ""1258 1259 #. translators: %s: contact type, for example "email" or "phone".1260 #: premium/CRM/CrmStore.php:6161261 #, php-format1262 msgid "Contact (%s)"1263 msgstr ""1264 1265 #: premium/CRM/CrmStore.php:6201266 msgid "Ninhle AI Sales Assistant customer profile"1267 msgstr ""1268 1269 #: premium/CRM/CrmStore.php:6231270 msgid "Ninhle AI Sales Assistant contact permissions"1271 msgstr ""1272 1273 #: premium/CRM/CrmStore.php:6241274 msgid "Purpose"1275 msgstr ""1276 1277 #: premium/CRM/CrmStore.php:6271278 msgid "Source"1279 msgstr ""1280 1281 #: premium/CRM/CrmStore.php:6281282 msgid "Recorded"1283 msgstr ""1284 1285 #: premium/CRM/CrmStore.php:6341286 msgid "Ninhle AI Sales Assistant purchase opportunities"1287 msgstr ""1288 1289 #: premium/CRM/CrmStore.php:6351290 msgid "Need"1291 msgstr ""1292 1293 #: premium/CRM/CrmStore.php:6361294 msgid "Stage"1295 msgstr ""1296 1297 #: premium/CRM/CrmStore.php:6371298 msgid "Payment"1299 msgstr ""1300 1301 #: premium/CRM/CrmStore.php:6381302 msgid "Products"1303 msgstr ""1304 1305 #: premium/CRM/CrmStore.php:6641306 msgid "Ninhle AI Sales Assistant customer history"1307 msgstr ""1308 1309 #: premium/CRM/CrmStore.php:6651310 #: premium/CRM/CrmStore.php:6751311 msgid "Type"1312 msgstr ""1313 1314 #: premium/CRM/CrmStore.php:6671315 msgid "Content"1316 msgstr ""1317 1318 #: premium/CRM/CrmStore.php:6741319 msgid "Ninhle AI Sales Assistant support and follow-up requests"1320 msgstr ""1321 1322 #: premium/CRM/CrmStore.php:6771323 msgid "Resolution"1324 msgstr ""1325 1326 #: premium/Integrations/TelegramService.php:771327 msgid "None"1328 msgstr ""1329 1330 #. translators: %s: chat widget brand name.1331 #: premium/Integrations/TelegramService.php:791332 #, php-format1333 msgid "New lead from %s"1334 msgstr ""1335 1336 #: premium/Integrations/TelegramService.php:801337 msgid "Name:"1338 msgstr ""1339 1340 #: premium/Integrations/TelegramService.php:811341 msgid "Email:"1342 msgstr ""1343 1344 #: premium/Integrations/TelegramService.php:821345 msgid "Phone:"1346 msgstr ""1347 1348 #: premium/Webhooks/WebhookDispatcher.php:1131349 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 1 1 <?php 2 2 /** 3 * Plugin Name: Ninhle AI Sales Assistant for WooCommerce4 * Description: AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff.5 * Version: 2. 5.03 * 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 6 6 * Author: Ninhle 7 7 * Author URI: https://ninhle.com … … 37 37 38 38 // Define Plugin Constants 39 define('SMBCOMMERCE_VERSION', '2. 5.0');39 define('SMBCOMMERCE_VERSION', '2.6.0'); 40 40 define('SMBCOMMERCE_FILE', __FILE__); 41 41 define('SMBCOMMERCE_PATH', plugin_dir_path(__FILE__)); -
ninhle-ai-sales-assistant/tags/2.6.0/package-lock.json
r3761175 r3765552 1 1 { 2 2 "name": "ninhle-ai-sales-assistant-frontend", 3 "version": "2. 5.0",3 "version": "2.6.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "ninhle-ai-sales-assistant-frontend", 9 "version": "2. 5.0",9 "version": "2.6.0", 10 10 "dependencies": { 11 11 "preact": "^10.23.1", -
ninhle-ai-sales-assistant/tags/2.6.0/package.json
r3761175 r3765552 2 2 "name": "ninhle-ai-sales-assistant-frontend", 3 3 "private": true, 4 "version": "2. 5.0",4 "version": "2.6.0", 5 5 "type": "module", 6 6 "scripts": { -
ninhle-ai-sales-assistant/tags/2.6.0/readme.txt
r3762743 r3765552 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 2. 5.07 Stable tag: 2.6.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 AI chatbot and shopping assistant for WooCommerce: answers from your real catalog, tracks orders, hands off to your team, shows AI-assisted orders.11 AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in chat, tracks orders and shows why shoppers don't buy. 12 12 13 13 == Description == 14 14 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. 16 16 17 17 Shoppers 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. … … 19 19 = Get started in three steps = 20 20 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.21 1. **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. 22 22 2. **Sync your catalog.** One click, or `wp ninhle-ai-sales-assistant sync-products`, and the assistant knows your products, prices and stock. 23 23 3. **Go live.** The chat appears on your store with your name, logo, colour and suggested questions. 24 24 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 27 Product 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 29 In 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 33 Product 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. 32 34 33 35 = Order tracking in the chat = 34 36 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 41 The 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 45 The 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 47 In 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. 36 48 37 49 = A person is one tap away = 38 50 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. 40 52 41 53 = See the orders the chatbot helped close = … … 43 55 The 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. 44 56 57 = Ready for AI agents (MCP) = 58 59 On 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 45 61 = Your key, your data, no per-conversation fee = 46 62 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.63 Use 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. 48 64 49 65 = Everything in the free WooCommerce chatbot = 50 66 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. 66 83 67 84 = Optional paid plans = … … 70 87 plugin; nothing in the free version is locked or time-limited. 71 88 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; 90 nightly AI labels of why shoppers did not buy, with a weekly email summary; a customer card in 91 the Inbox; product variations with per-variation add-to-cart links; Telegram notifications for 92 new leads and handoffs; a sales dashboard; cached answers for repeated questions; and the 93 revenue of orders that followed a chat, per currency, on the AI Value & Performance tab. 76 94 77 95 **Pro** — everything in Starter, plus coupon validation, automatic failover to a … … 91 109 conversation history, your sales playbook and business information, and product data from 92 110 your own store, each time a visitor sends a message. Nothing is sent in local-only mode. 111 If you choose "WordPress AI connection" (WordPress 7.0+), the same data goes through WordPress 112 to the provider you connected in Settings > Connectors, under that provider's terms. 93 113 94 114 * Anthropic (Claude) — https://www.anthropic.com/legal/privacy — https://www.anthropic.com/legal/consumer-terms 95 115 * OpenAI — https://openai.com/policies/privacy-policy — https://openai.com/policies/terms-of-use 96 116 * 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 119 version contains no code for this. If you turn it on, once a night the text of finished chats 120 that did not lead to an order is sent to your AI provider, with email addresses and phone 121 numbers replaced by placeholders, so the AI can label why the shopper did not buy. A monthly 122 spending limit you set stops the job. 97 123 98 124 **Link reading (optional, off by default)** — when you enable "Let the bot read links … … 154 180 permission_callback, and the endpoints the AI uses as tools are internal-only - 155 181 they cannot be reached from outside the site. Visitor sessions use hashed tokens 156 and chat messages are rate limited per session. 182 and chat messages are rate limited per session. When a background AI reply is started 183 through a loopback request, that request is signed and expires after 60 seconds. 184 185 The plugin server never changes a shopper's cart: "Add to cart" in the chat calls 186 the WooCommerce Store API from the shopper's own browser. The AI can only propose 187 products that come from your catalog, and the card always reads the current price 188 and stock from WooCommerce. 157 189 158 190 Chat messages are rendered as DOM elements rather than injected HTML, and only … … 178 210 paragraph about this to Settings > Privacy. 179 211 212 Sales Insights stores the questions the assistant could not answer, with email addresses 213 and phone numbers hidden, and in the Starter plan the reason a chat did not lead to an 214 order with a short quote. When a logged-in customer chats, or a customer proves they own 215 an order, the conversation records that account or order number, so your team can see 216 the customer card (Starter plan). The language of each conversation is stored to reply in 217 it. All of this is included in the personal-data export and removed by the eraser. 218 219 In the visitor's browser, the chat keeps its session token and, for the new-replies badge, 220 the conversation number, the last message seen and when it was seen (localStorage). No 221 message text is stored there. 222 180 223 == Installation == 181 224 … … 192 235 AI provider for your own API key, or use local-only mode with no key at all. 193 236 237 = Can this WooCommerce chatbot add products to the cart? = 238 239 Yes. Product answers show cards with an Add to cart button, and the cart is updated by WooCommerce 240 in the shopper's browser, so your prices, stock and cart rules apply. In the free version, products 241 with options link to the product page; the Starter plan lets shoppers choose options and quantities 242 on the card. Nothing is added until the shopper taps the button. 243 194 244 = Can customers track their orders in the chat? = 195 245 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. 246 Yes. The assistant shows an order card with the status, items and tracking number. Logged-in 247 customers can ask for their recent orders. Guests give the order number together with the email 248 address 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 252 Yes, by default on new installs. It detects the language of each message and answers in it. Sites 253 upgraded from an earlier version keep replying in the store's language until you change "Reply 254 language" in Settings. 199 255 200 256 = Which AI models does the chatbot work with? = 201 257 202 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. You choose the 258 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. On WordPress 7.0 and 259 later you can also use the AI provider connected in Settings > Connectors. You choose the 203 260 provider and can switch at any time. 204 261 262 = What does Sales Insights show? = 263 264 The questions shoppers asked that the assistant could not answer, grouped and ranked, what they 265 searched for but could not find, and which products they wanted while out of stock. You can add 266 an answer from the same screen. The Starter plan adds the reasons chats did not lead to an order and a 267 weekly email summary. 268 269 = Can AI agents read my product catalog (MCP)? = 270 271 Yes, if you allow it. On WordPress 6.9 and later, turn on "AI agents" in Settings: the plugin 272 registers read-only abilities for product search, product details and store policies. With the 273 MCP Adapter plugin, MCP clients can use them. Orders, carts and customer data are never shared. 274 205 275 = Does the shopping assistant recommend products? = 206 276 207 277 Yes. 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.278 asked for and shows them as cards. It only recommends products that exist in your store. 209 279 210 280 = Do I need a paid AI subscription? = … … 213 283 and are billed directly by that provider. Local-only mode needs no key. 214 284 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 287 It depends on the provider and model you choose, and you pay the provider directly. In our test 288 of 60 conversations on a real store with version 2.6.0, GPT-4o mini cost about US$0.0009 per 289 conversation and Gemini 3.8 Flash about US$0.015. The AI Value & Performance tab shows what your 290 own store actually spent. 220 291 221 292 = Can I try it without an API key? = 222 293 223 Yes. Local-only mode answers product, cart and order questions from your store data without224 calling any AI provider. Add a key later when you want full conversations.294 Yes. Local-only mode answers greetings, store policies, cart and order questions from your store 295 data without calling any AI provider. Add a key later when you want full conversations. 225 296 226 297 = Will it slow down my store? = 227 298 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. 299 The chat button is small. The chat window, product cards, cart view and voice input load only 300 when they are needed, and AI replies run in the background after a message is sent. While the 301 chat is minimized after a recent conversation, it checks for new replies every 25 seconds, only 302 while the browser tab is visible. Nothing is loaded from third-party servers unless you pick a 303 Google font. 230 304 231 305 = Is my store data sent to you? = … … 237 311 = Does it work without WooCommerce? = 238 312 239 The chat and lead capture work on any WordPress site. Product search, catalog grounding and240 order lookup require WooCommerce.313 The chat and lead capture work on any WordPress site. Product search, product cards, catalog 314 grounding and order lookup require WooCommerce. 241 315 242 316 = Will it invent prices or stock? = 243 317 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. 318 No. Product facts come from lookups against your store, and product cards read the current price 319 and stock from WooCommerce when the reply is shown. If the catalog has not been synced, the bot 320 is told to say so and offer a handoff instead of guessing. 246 321 247 322 = What counts as an "AI-assisted order"? = … … 249 324 An order placed within your attribution window (24 hours, 72 hours or 7 days) after the 250 325 customer 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 left252 out, and partial refunds are deducted. AI-assisted does not mean the AI was the only reason 253 for the order.326 in the chat, or added a product to the cart from the chat. Each order is counted once. 327 Cancelled, failed and fully refunded orders are left out, and partial refunds are deducted. 328 AI-assisted does not mean the AI was the only reason for the order. 254 329 255 330 = Is anything in the free version locked? = … … 274 349 275 350 == 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. 276 375 277 376 = 2.5.0 = … … 370 469 == Upgrade Notice == 371 470 471 = 2.6.0 = 472 Add 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 372 474 = 2.5.0 = 373 475 Replies 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. … … 378 480 = 2.4.0 = 379 481 New 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 2 2 import { api } from '../api'; 3 3 import { ConfirmBox, NoticeList, useConfirm, useNotices } from './Feedback'; 4 import { CustomerCard, CustomerCardData } from './CustomerCard'; 4 5 5 6 // Nhãn hiển thị (2.5.0): chủ shop đọc "Waiting for your team" thay vì mã human_active. … … 24 25 }; 25 26 26 export function Inbox() { 27 /** openId: mở thẳng hội thoại này (link "Open chat" trong Sales Insights). */ 28 export function Inbox({ openId = null }: { openId?: number | null } = {}) { 27 29 const { notices, push, dismiss } = useNotices(); 28 30 const confirmation = useConfirm(); … … 37 39 const timeoutRef = useRef<number | null>(null); 38 40 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 }; 39 50 40 51 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]); 41 59 42 60 const loadConversations = async () => { … … 49 67 const selectConversation = async (conv: any) => { 50 68 setActiveConv(conv); 69 shownIdRef.current = conv.id; 70 setCustomer(null); 71 loadCustomer(conv.id); 51 72 try { 52 73 const data = await api.getMessages(conv.id); … … 68 89 scrollToBottom(); 69 90 pollIntervalRef.current = 5000; 91 loadCustomer(activeConv.id); 70 92 } 71 93 } catch { pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000); } … … 86 108 try { 87 109 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; } 89 111 setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; }); 90 112 loadConversations(); … … 111 133 try { 112 134 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; } 114 136 setSelectedIds(new Set()); 115 137 loadConversations(); … … 197 219 </div> 198 220 </div> 221 {customer && ( 222 <details className="smb-customer-inline"> 223 <summary>Customer: {customer.name || 'Guest customer'}</summary> 224 <CustomerCard card={customer} /> 225 </details> 226 )} 199 227 <div className="smb-commerce-chat-history"> 200 228 {messages.map(m => ( … … 221 249 )} 222 250 </div> 251 {activeConv && customer && ( 252 <aside className="smb-customer-aside" aria-label="Customer"> 253 <CustomerCard card={customer} /> 254 </aside> 255 )} 223 256 </div> 224 257 </> -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/components/Settings.tsx
r3761175 r3765552 38 38 smbcommerce_hide_on_checkout: '0', 39 39 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', 40 47 smbcommerce_gemini_model: '', 41 48 smbcommerce_anthropic_model: '', … … 74 81 const [features, setFeatures] = useState<any>({}); 75 82 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); 76 87 const [modelReport, setModelReport] = useState<any>(null); 77 88 // 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. … … 179 190 smbcommerce_hide_on_checkout: data.settings.smbcommerce_hide_on_checkout ?? '0', 180 191 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', 181 199 smbcommerce_gemini_model: data.settings.smbcommerce_gemini_model || '', 182 200 smbcommerce_anthropic_model: data.settings.smbcommerce_anthropic_model || '', … … 199 217 setFeatures(data.settings.smbcommerce_features || {}); 200 218 setPremiumBuild(data.settings.smbcommerce_premium_build !== false); 219 setWpAiAvailable(data.settings.smbcommerce_wp_ai_available === true); 220 setAbilitiesSupported(data.settings.smbcommerce_abilities_supported === true); 201 221 setModelReport(data.settings.smbcommerce_model_report || null); 202 222 setSampleBusinessInfo(data.settings.smbcommerce_business_info_is_sample ? data.settings.smbcommerce_business_info : null); … … 247 267 smbcommerce_hide_on_checkout: settings.smbcommerce_hide_on_checkout, 248 268 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, 249 276 smbcommerce_gemini_model: settings.smbcommerce_gemini_model, 250 277 smbcommerce_anthropic_model: settings.smbcommerce_anthropic_model, … … 562 589 563 590 <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"> 564 609 <label>AI provider</label> 565 610 <select … … 570 615 <option value="anthropic">Anthropic (Claude 3.5)</option> 571 616 <option value="gemini">Google Gemini</option> 617 {(wpAiAvailable || settings.smbcommerce_ai_provider === 'wordpress') && ( 618 <option value="wordpress">WordPress AI connection (Settings → Connectors)</option> 619 )} 572 620 </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 )} 573 628 </div> 574 629 … … 1041 1096 1042 1097 <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"> 1043 1194 <h3><span className="dashicons dashicons-admin-tools"></span> Advanced features</h3> 1044 1195 -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/index.tsx
r3752682 r3765552 1 import React, { use State } from 'react';1 import React, { useEffect, useState } from 'react'; 2 2 import { createRoot } from 'react-dom/client'; 3 3 import { Inbox } from './components/Inbox'; … … 6 6 import { Settings } from './components/Settings'; 7 7 import { ValueDashboard } from './components/ValueDashboard'; 8 import { loadInsights } from './insightsLoader'; 9 import { t } from './i18n'; 8 10 import './style.css'; 9 11 10 const TABS = ['value', 'in box', 'rules', 'leads'];12 const TABS = ['value', 'insights', 'inbox', 'rules', 'leads']; 11 13 const TAB_KEY = 'smbcommerce_admin_tab'; 12 14 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. */ 14 16 function initialTab() { 17 const fromUrl = new URLSearchParams(window.location.search).get('smb_tab'); 18 if (fromUrl && TABS.includes(fromUrl)) return fromUrl; 15 19 try { 16 20 const saved = window.localStorage.getItem(TAB_KEY); … … 23 27 const AdminApp = () => { 24 28 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); 25 32 const setActiveTab = (tab: string) => { 26 33 setActiveTabState(tab); … … 28 35 }; 29 36 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 ); 30 53 31 54 return ( … … 33 56 <h1>Ninhle AI Sales Assistant Admin</h1> 34 57 <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')} 39 63 </h2> 40 64 <div style={{ marginTop: 20 }}> 41 65 {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} />} 43 72 {activeTab === 'rules' && <Rules />} 44 73 {activeTab === 'leads' && <Leads />} -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/admin/style.css
r3752682 r3765552 374 374 } 375 375 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 376 440 .smb-commerce-main-input { 377 441 padding: 15px; … … 656 720 outline-offset: 2px; 657 721 } 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 40 40 }, 41 41 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 */ 42 46 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`, { 44 54 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 51 57 }); 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 } 52 64 if (!res.ok) throw new Error('Failed to send message'); 53 65 return await res.json(); … … 69 81 * 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. 70 82 */ 71 event(conversationId: number, type: string, ref = '' ) {83 event(conversationId: number, type: string, ref = '', extra: Record<string, number> = {}) { 72 84 fetch(`${BASE_URL}/chat/event`, { 73 85 method: 'POST', 74 86 headers: this.getHeaders(), 75 body: JSON.stringify({ conversation_id: conversationId, type, ref }),87 body: JSON.stringify({ conversation_id: conversationId, type, ref, ...extra }), 76 88 keepalive: true 77 89 }).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 8 8 config: any; 9 9 buttonRef?: Ref<HTMLButtonElement>; 10 /** Số tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */ 11 unread?: number; 10 12 } 11 13 12 export function ChatButton({ onClick, onIntent, config, buttonRef }: ChatButtonProps) {14 export function ChatButton({ onClick, onIntent, config, buttonRef, unread = 0 }: ChatButtonProps) { 13 15 return ( 14 16 <button … … 20 22 onTouchStart={onIntent} 21 23 onFocus={onIntent} 22 aria-label={ t('openChat')}24 aria-label={unread > 0 ? `${t('openChat')} (${t('unreadMessages').replace('%d', String(unread))})` : t('openChat')} 23 25 aria-haspopup="dialog" 24 26 > … … 30 32 </svg> 31 33 )} 34 {unread > 0 && <span className="smb-unread" aria-hidden="true">{unread > 9 ? '9+' : unread}</span>} 32 35 </button> 33 36 ); -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/components/ChatWindow.tsx
r3761175 r3765552 6 6 import { toPlainText } from '../markdown'; 7 7 import { t } from '../i18n'; 8 import { nextDelay, nextError, nextIdle, POLL } from '../pollSchedule'; 8 9 import type { VoiceSession } from '../voice'; 9 10 10 11 type VoiceModule = typeof import('../voice'); 12 type CardsModule = typeof import('./ProductCards'); 13 type 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 */ 19 function 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 } 11 24 12 25 /** 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ó). */ … … 50 63 order: 'stageOrder', 51 64 handoff: 'stageHandoff', 65 queued: 'stageQueued', 52 66 }; 53 67 … … 108 122 const [listening, setListening] = useState(false); 109 123 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); 110 131 111 132 const voiceModRef = useRef<VoiceModule | null>(null); … … 118 139 const cancelClearRef = useRef<HTMLButtonElement>(null); 119 140 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'); 121 146 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); 122 151 123 152 const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; … … 128 157 129 158 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]); 130 165 131 166 // 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 … … 181 216 182 217 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; } 184 219 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; 186 222 }; 187 223 188 224 const poll = async () => { 189 225 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); 191 228 return; 192 229 } 230 let gotMessages = false; 193 231 try { 194 232 const data = await api.pollMessages(conversationId, lastMessageIdRef.current); … … 201 239 const incoming = data.messages.filter((m: any) => m.sender_type !== 'user'); 202 240 if (incoming.length > 0) { 241 awaitingSinceRef.current = 0; 203 242 setIsTyping(false); 204 243 setStage(null); … … 206 245 setLiveText(toPlainText(incoming[incoming.length - 1].content).slice(0, 300)); 207 246 } 208 pollIntervalRef.current = 5000;247 gotMessages = true; 209 248 } 249 errorDelayRef.current = 0; 250 idleDelayRef.current = nextIdle(idleDelayRef.current, gotMessages); 210 251 } 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 }); 215 265 216 266 useEffect(() => { … … 219 269 const handleVisibility = () => { 220 270 if (document.visibilityState === 'visible') { 221 pollIntervalRef.current = 5000; 271 idleDelayRef.current = POLL.idleStart; 272 errorDelayRef.current = 0; 222 273 if (timeoutRef.current) clearTimeout(timeoutRef.current); 223 274 poll(); … … 232 283 233 284 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. 235 286 if (timeoutRef.current) clearTimeout(timeoutRef.current); 236 287 timeoutRef.current = window.setTimeout(poll, 100); … … 244 295 }; 245 296 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. 247 298 const voiceLang = (mod: VoiceModule) => mod.pickLanguage( 248 document.documentElement.lang,299 languageRef.current || document.documentElement.lang, 249 300 navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language] 250 301 ); … … 300 351 const savedMsg = await api.sendMessage(conversationId, content, crypto.randomUUID()); 301 352 setMessages(prev => prev.map(m => m.id === tempMsg.id ? savedMsg : m)); 353 if (status === 'bot_active') awaitingSinceRef.current = Date.now(); 302 354 pollSoon(); 303 355 } catch (err) { … … 372 424 373 425 // 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 374 440 const trackLink = (url: string) => { 375 441 try { … … 445 511 </div> 446 512 <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 ) : ( 448 520 <a className="smb-icon-btn" href={config.cartUrl} aria-label={t('cart')} title={t('cart')}><Icon d={ICONS.cart} /></a> 449 ) }521 ))} 450 522 <button type="button" className="smb-icon-btn smb-expand-btn" onClick={() => setExpanded(!expanded)} 451 523 aria-pressed={expanded} aria-label={expanded ? t('collapse') : t('expand')} title={expanded ? t('collapse') : t('expand')}> … … 471 543 472 544 <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 )} 473 549 {config.aiNotice && <p className="smb-ai-notice">{config.aiNotice}</p>} 474 550 … … 484 560 )} 485 561 486 {messages.map(m => (562 {messages.map(m => [ 487 563 <div key={m.id} className={`smb-msg ${m.sender_type === 'user' ? 'is-user' : m.sender_type === 'agent' ? 'is-agent' : 'is-bot'}`}> 488 564 <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 ])} 491 574 492 575 {isTyping && ( -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/i18n.ts
r3761175 r3765552 11 11 const FALLBACK: Strings = { 12 12 openChat: 'Open chat', 13 unreadMessages: 'new replies: %d', 13 14 chatWindow: 'Chat with our assistant', 14 15 cart: 'Cart', … … 50 51 stageOrder: 'Looking up your order…', 51 52 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', 52 85 resolutionQuestion: 'Did this answer your question?', 53 86 yes: 'Yes', -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/index.tsx
r3761175 r3765552 4 4 import { api } from './api'; 5 5 import { onColor } from './color'; 6 import { PEEK_MS, countUnread, lastId, mergeMessages, readSeen, shouldPeek, writeSeen } from './peek'; 6 7 import './style.css'; 7 8 … … 41 42 const wasOpen = useRef(false); 42 43 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 }, []); 43 89 44 90 const prefetchChat = () => { … … 232 278 /> 233 279 ) : ( 234 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} />280 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} unread={unread} /> 235 281 )} 236 282 </div> -
ninhle-ai-sales-assistant/tags/2.6.0/src/JS/widget/style.css
r3761175 r3765552 91 91 92 92 .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 } 94 113 .smb-launcher img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; } 95 114 … … 247 266 .is-bot .smb-bubble { background: var(--smb-bg-soft); color: var(--smb-text); border-bottom-left-radius: 6px; } 248 267 .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; } 249 364 250 365 .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 25 25 vi: 'vi-VN', en: 'en-US', th: 'th-TH', id: 'id-ID', ms: 'ms-MY', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN', 26 26 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', 28 28 }; 29 29 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/AIProviderFactory.php
r3747521 r3765552 13 13 14 14 /** Khoá setting chứa API key tương ứng mỗi provider. */ 15 p rivatestatic function key_setting($provider) {15 public static function key_setting($provider) { 16 16 $map = [ 17 17 'openai' => 'smbcommerce_openai_key', … … 20 20 ]; 21 21 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)); 22 31 } 23 32 … … 37 46 if (class_exists('\\Smartbuy\SMBCommerce\\AI\\Providers\\GeminiProvider')) { 38 47 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(); 39 54 } 40 55 break; … … 65 80 $chain = []; 66 81 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)) { 69 83 continue; 70 84 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/AIService.php
r3752682 r3765552 87 87 $user_context .= "No user info provided yet. Do not assume their name.\n"; 88 88 } 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 } 90 95 91 96 // 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. … … 100 105 // WooCommerce Product RAG Context 101 106 $product_context = ''; 107 $context_ids = []; 102 108 if (class_exists('\\Smartbuy\SMBCommerce\\WooCommerce\\WooCommerceProductScanner')) { 103 109 $scanner = new \Smartbuy\SMBCommerce\WooCommerce\WooCommerceProductScanner(); … … 105 111 $latest_user_message !== '' ? [$latest_user_message] : [] 106 112 ); 113 $context_ids = $scanner->context_ids(); 107 114 } 108 115 … … 123 130 $sales_playbook = SalesPlaybook::build($playbook_state); 124 131 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()); 126 136 127 137 // Kịch bản đứng đầu để định hình vai trò, sau đó mới tới ràng buộc và dữ liệu. … … 130 140 $guardrails, 131 141 $capability_limits, 142 $cart_actions_prompt, 132 143 $master_prompt, 133 144 $handoff_instructions, 134 145 $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, 135 150 $user_context, 136 151 $history_digest, … … 152 167 $final_system_prompt .= "\n\n[SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES]\n" . $extracted_content; 153 168 } 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; 154 176 } 155 177 … … 187 209 188 210 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(); 189 213 $local_reply = \Smartbuy\SMBCommerce\Commerce\LocalHandler::handle($routing); 190 214 191 215 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); 193 219 self::log_ai_request($conversation_id, 'chat', 0, 0, 0, 'local', false, $routing['reason']); 194 220 return true; … … 237 263 \Smartbuy\SMBCommerce\Commerce\RoutingReason::UNRESOLVED_LOCAL_ONLY 238 264 ); 265 \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'local_only'); 239 266 return true; 240 267 } … … 318 345 $rounds = 0; 319 346 $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; 320 349 // Gom kết quả tool để đối chiếu số liệu trong câu trả lời cuối. 321 350 $grounding_from_tools = ''; … … 329 358 'contact_id' => (int) ($conversation['contact_id'] ?? 0), 330 359 ]); 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); 331 364 332 365 while (!empty($tool_calls) && $rounds < self::MAX_TOOL_ROUNDS) { … … 341 374 self::set_stage($conversation_id, self::STAGE_BY_TOOL[$tool_name] ?? 'thinking'); 342 375 $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); 343 378 344 379 // Gọi tool handoff nghĩa là thật sự phải chuyển cho người thật, … … 346 381 if ($tool_name === 'create_handoff' && !empty($result['success'])) { 347 382 $tool_handoff_priority = $tool_params['priority'] ?? 'normal'; 383 $handoff_missing_info = ($tool_params['cause'] ?? '') === 'missing_information'; 348 384 } 349 385 … … 369 405 // Provider tự dựng message theo định dạng của mình. 370 406 $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 } 371 414 372 415 $call_start = microtime(true); … … 396 439 $reply_content = self::clean_reply($raw_reply); 397 440 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 398 452 // Hết vòng mà model vẫn đòi gọi tool: đừng trả về câu rỗng cho khách. 399 453 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'); 401 456 } 402 457 … … 450 505 } 451 506 } 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 } 452 519 453 520 // --- Đối chiếu mọi con số tiền với dữ liệu thật đã đưa cho model --- … … 474 541 $saved_reply = MessageRepository::add_message($conversation_id, 'system', $reply_content, null, $metadata); 475 542 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 } 476 551 477 552 // Chỉ cache khi lượt này không gọi tool nào — kết quả tool là dữ liệu sống. … … 535 610 'get_product_variations' => 'products', 536 611 'get_order_status' => 'order', 612 'get_my_orders' => 'order', 537 613 'get_customer_downloads' => 'order', 538 614 'create_handoff' => 'handoff', … … 679 755 * HỢP LỆ, bot trả lời "mã không hợp lệ". Trả về '' khi gói đã có đủ công cụ. 680 756 */ 681 private static function capability_limits( ) {757 private static function capability_limits($has_coupon_offers = false) { 682 758 $gate = '\\Smartbuy\\SMBCommerce\\Licensing\\LicenseGate'; 683 759 $limits = []; 684 760 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].' : ''); 686 763 } 687 764 if (!$gate::can('product_variations')) { … … 726 803 * và retry_count; rỗng với lượt không gọi AI (local, cache, local_only). 727 804 */ 728 p rivatestatic 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 = []) { 729 806 global $wpdb; 730 807 $table = $wpdb->prefix . SMBCOMMERCE_PREFIX . 'ai_logs'; -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/ClaudeProvider.php
r3752682 r3765552 172 172 } 173 173 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 174 215 public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) { 175 216 if (!empty($assistant_message)) { … … 199 240 ], 200 241 '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, 202 244 ]); 203 245 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/GeminiProvider.php
r3752682 r3765552 19 19 // ngừng lần lượt và khi đó mọi lượt chat đều lỗi 404 (đo trên staging 2026-10-09). 20 20 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; 21 24 22 25 private $model = self::DEFAULT_MODEL; … … 116 119 117 120 $response = $this->call_api($payload); 118 121 119 122 if (is_wp_error($response)) { 120 123 return $response; 121 124 } 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 } 126 151 } 127 152 … … 136 161 $meta = $response['usageMetadata'] ?? []; 137 162 $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'], 141 166 'cache_write_tokens' => 0, 142 167 'model' => (string) ($response['modelVersion'] ?? $this->model), … … 149 174 'usage' => $usage, 150 175 '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 ], 151 233 ]; 152 234 } … … 188 270 } 189 271 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) { 191 276 $url = $this->build_url(); 192 277 … … 194 279 'headers' => $this->build_headers(), 195 280 '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, 197 283 ]); 198 284 … … 213 299 \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini API Error (HTTP ' . $http_code . '): ' 214 300 . 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 } 215 310 return new \WP_Error('gemini_error', $err_msg . ' (HTTP ' . $http_code . ')'); 216 311 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/Providers/OpenAIProvider.php
r3752682 r3765552 182 182 } 183 183 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 184 223 public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) { 185 224 if (!empty($assistant_message)) { … … 204 243 ], 205 244 '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, 207 247 ]); 208 248 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ResponseSanitizer.php
r3752682 r3765552 38 38 'IMPORTANT RULES', 39 39 "SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES", 40 // 2.6.0 41 'REPLY LANGUAGE', 42 'PRODUCT CARDS', 40 43 ]; 41 44 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/SalesPlaybook.php
r3752682 r3765552 263 263 'hesitating' => self::contains($text, ['suy nghĩ', 'cân nhắc', 'để xem', 'từ từ', 'chưa cần', 'tính sau']), 264 264 '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']), 266 268 'asks_policy' => self::contains($text, ['bảo hành', 'đổi trả', 'ship', 'giao hàng', 'vận chuyển', 'hoàn tiền']), 267 269 ]; … … 275 277 } 276 278 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'])) { 278 283 $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.'; 279 284 } elseif (!empty($s['price_concern'])) { … … 311 316 } 312 317 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 313 324 private static function contains($haystack, $needles) { 314 325 foreach ($needles as $needle) { -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ToolExecutor.php
r3747521 r3765552 38 38 'get_product_variations' => '/smb-commerce/v1/tools/product-variations', 39 39 'check_coupon' => '/smb-commerce/v1/tools/check-coupon', 40 'propose_cart_action' => '/smb-commerce/v1/tools/propose-cart-action', 40 41 'get_order_status' => '/smb-commerce/v1/tools/order-status', 42 'get_my_orders' => '/smb-commerce/v1/tools/my-orders', 41 43 'get_customer_downloads' => '/smb-commerce/v1/tools/customer-downloads', 42 44 'get_policy' => '/smb-commerce/v1/tools/policy', -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/AI/ToolsHelper.php
r3747521 r3765552 19 19 'get_product_variations' => 'product_variations', 20 20 'check_coupon' => 'coupon_tools', 21 'propose_cart_action' => 'cart_actions_advanced', 21 22 ]; 22 23 $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 } 24 30 if (!isset($paid_tools[$tool['name']])) { 25 31 return true; … … 110 116 ], 111 117 [ 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 [ 112 154 'name' => 'get_order_status', 113 155 '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.', … … 141 183 ], 142 184 [ 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 [ 143 195 'name' => 'get_customer_downloads', 144 196 '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.', … … 201 253 'enum' => ['low', 'normal', 'high', 'urgent'], 202 254 '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.' 203 261 ], 204 262 'email' => [ -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Admin/AdminMenu.php
r3752682 r3765552 75 75 'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')), 76 76 '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(), 79 79 ]; 80 80 … … 84 84 $config 85 85 ); 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 ]; 86 173 } 87 174 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Analytics/OrderAttribution.php
r3752682 r3765552 24 24 const META_INTERACTION_AT = '_smbcommerce_interaction_at'; 25 25 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 26 29 public static function init() { 27 30 add_action('woocommerce_checkout_order_processed', [self::class, 'on_classic_checkout'], 20, 3); … … 32 35 33 36 /** 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ờ. 37 42 * @return bool true nếu đã lưu được vào phiên. 38 43 */ 39 public static function remember_interaction($conversation_id ) {44 public static function remember_interaction($conversation_id, $at = null) { 40 45 $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')) { 42 59 return false; 43 60 } … … 66 83 $wc->session->set(self::SESSION_KEY, [ 67 84 'conversation_id' => $conversation_id, 68 'at' => gmdate('Y-m-d H:i:s'),85 'at' => $at, 69 86 ]); 70 87 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); 71 108 } 72 109 … … 220 257 221 258 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(); 223 260 } 224 261 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Analytics/ValueEvents.php
r3752682 r3765552 24 24 const ORDER_STATUS = 'order_status'; 25 25 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'; 26 36 27 37 /** 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 187 187 'email' => $params['email'] ?? '', 188 188 'phone' => $params['phone'] ?? '', 189 'user_id' => (int) ($params['user_id'] ?? 0), 189 190 ]); 190 191 … … 210 211 $order = $result->data(); 211 212 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 212 216 return sprintf( 213 217 /* translators: 1: order number. 2: order status. 3: date the order was placed. */ 214 218 __('Order %1$s is currently: %2$s (placed on %3$s).', 'ninhle-ai-sales-assistant'), 215 219 $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'] 218 225 ); 219 226 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/LocalResolver.php
r3747521 r3765552 75 75 76 76 /** 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 /** 77 89 * Quyết định định tuyến cho một tin nhắn. 78 90 * … … 124 136 } 125 137 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)) { 128 142 return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'checkout'); 129 143 } 130 144 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)) { 132 147 return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'view_cart'); 133 148 } … … 178 193 private static function length($text) { 179 194 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; 180 206 } 181 207 … … 253 279 } 254 280 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 255 287 if (empty($proof)) { 256 288 return null; -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/CartService.php
r3747521 r3765552 18 18 * Vì sao điều đó nguy hiểm ở đây: 19 19 * 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. 34 35 * 35 36 * Nguyên tắc: chỉ động vào giỏ khi chắc chắn đang ở trong request của chính … … 252 253 253 254 /** 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. 259 257 */ 260 258 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(); 278 260 } 279 261 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/OrderService.php
r3747521 r3765552 33 33 const PUBLIC_FIELDS = [ 34 34 'id', 'order_number', 'status', 'date', 'total', 'currency', 'items', 35 'payment_method', 'shipping_method', 35 'payment_method', 'shipping_method', 'tracking', 36 36 ]; 37 37 … … 134 134 135 135 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)]); 136 171 } 137 172 … … 278 313 'payment_method' => $order->get_payment_method_title(), 279 314 '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)), 280 319 ]; 281 320 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Commerce/Woo/ProductImage.php
r3761175 r3765552 21 21 const SIZES = ['large', 'medium_large', 'medium', 'full']; 22 22 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 23 26 /** 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. 25 29 * @return string URL dùng được, hoặc chuỗi rỗng khi không có ảnh nào còn trên đĩa. 26 30 */ 27 public static function url($attachment_id ) {31 public static function url($attachment_id, array $sizes = self::SIZES) { 28 32 $attachment_id = (int) $attachment_id; 29 33 if ($attachment_id <= 0 || !function_exists('wp_get_attachment_image_src')) { … … 34 38 $tried = []; 35 39 36 foreach ( self::SIZESas $size) {40 foreach ($sizes as $size) { 37 41 $src = wp_get_attachment_image_src($attachment_id, $size); 38 42 $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 57 57 // Danh mục rỗng khác hẳn với "không tìm thấy". Nói rõ để model khuyên 58 58 // 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 } 61 83 } 62 84 63 85 // 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'))); 72 166 } 73 167 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Conversation/ConversationRepository.php
r3752682 r3765552 30 30 } 31 31 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 32 44 public static function create_conversation($contact_id) { 33 45 global $wpdb; -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Deactivator.php
r3752682 r3765552 13 13 wp_clear_scheduled_hook('smbcommerce_run_jobs'); 14 14 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(); 15 17 16 18 flush_rewrite_rules(); -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/JobDispatcher.php
r3752682 r3765552 9 9 * Nối hàng đợi job với phần xử lý thật. 10 10 * 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. 15 18 */ 16 19 class JobDispatcher { … … 18 21 const CRON_HOOK = 'smbcommerce_run_jobs'; 19 22 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; 20 33 21 34 public static function init() { … … 52 65 } 53 66 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 } 54 84 try { 55 85 \Smartbuy\SMBCommerce\AI\AIService::process_user_message( … … 70 100 ); 71 101 } 102 } finally { 103 \Smartbuy\SMBCommerce\AI\TurnClock::clear(); 72 104 } 73 105 } … … 81 113 82 114 /** 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] 85 119 */ 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; 88 170 } 89 171 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/JobQueue.php
r3747521 r3765552 12 12 * (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. 13 13 * 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. 19 23 */ 20 24 class JobQueue { … … 25 29 const MAX_ATTEMPTS = 3; 26 30 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'; 29 42 30 43 /** Job bị coi là kẹt (worker chết giữa chừng) sau ngần này giây. */ 31 44 const STALE_LOCK_SECONDS = 180; 32 45 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 33 52 private static function table() { 34 53 global $wpdb; … … 36 55 } 37 56 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 */ 38 63 public static function enqueue($action, $payload = []) { 39 64 global $wpdb; 40 65 66 $payload = self::conversation_first((array) $payload); 41 67 $now = current_time('mysql'); 42 68 43 $ wpdb->insert(self::table(), [69 $inserted = $wpdb->insert(self::table(), [ 44 70 'action' => $action, 45 71 'payload' => wp_json_encode($payload), … … 50 76 ]); 51 77 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. 58 98 * 59 99 * @return array|null Job đã giành được, hoặc null nếu không có gì để làm. … … 61 101 public static function claim($filter_action = null) { 62 102 global $wpdb; 63 $table = self::table();64 103 65 104 self::release_stale_locks(); 66 105 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 } 73 109 74 110 // Lọc theo action là tuỳ chọn: chuỗi rỗng nghĩa là mọi action (câu SQL cố định). 75 111 $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 79 116 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(), 82 119 $now, 83 120 $action, … … 85 122 ), ARRAY_A); 86 123 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') 109 172 ), ARRAY_A); 110 173 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; 116 220 } 117 221 … … 151 255 'status' => 'pending', 152 256 'locked_by' => null, 153 'payload' => wp_json_encode( $payload),257 'payload' => wp_json_encode(self::conversation_first($payload)), 154 258 'next_attempt_at' => gmdate('Y-m-d H:i:s', strtotime(current_time('mysql')) + $delay), 155 259 'updated_at' => current_time('mysql'), … … 161 265 } 162 266 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 163 279 /** 164 280 * Rút và chạy tối đa $limit job. Trả về số job đã xử lý. … … 173 289 } 174 290 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); 182 292 $processed++; 183 293 } … … 186 296 } 187 297 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 188 329 private static function running_count() { 189 330 global $wpdb; 190 331 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; 191 360 } 192 361 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/MigrationManager.php
r3752682 r3765552 14 14 * 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, 15 15 * bảng value_events. 16 * 4 = 2.6.0: conversations.language (ngôn ngữ khách đang viết, S2). 16 17 */ 17 const SCHEMA_REVISION = 3;18 const SCHEMA_REVISION = 4; 18 19 19 20 /** Kết quả lần chuyển dữ liệu gần nhất, để chẩn đoán và cho test đọc. */ … … 64 65 // Cài mới: model đề xuất (chỉ khi benchmark đã duyệt). Nâng cấp không bao giờ đổi model đã lưu. 65 66 \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); 66 70 67 71 // Ngày bắt đầu đo của bảng "AI Value & Performance" (UTC): ghi một lần, không bao giờ ghi đè, … … 151 155 'outcome_at' => 'datetime DEFAULT NULL', 152 156 ]); 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 ]); 153 162 } 154 163 … … 208 217 outcome varchar(30) DEFAULT NULL, 209 218 outcome_at datetime DEFAULT NULL, 219 language varchar(16) DEFAULT NULL, 210 220 created_at datetime DEFAULT NULL, 211 221 updated_at datetime DEFAULT NULL, -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Plugin.php
r3752682 r3765552 45 45 } 46 46 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 47 52 // Domain-lock notice — chỉ hiển thị khi license.php tồn tại và domain không khớp. 48 53 // Bản phát triển (không có license.php) hoàn toàn im lặng. … … 78 83 \Smartbuy\SMBCommerce\Core\JobDispatcher::init(); 79 84 } 85 if (class_exists('\\Smartbuy\SMBCommerce\\Core\\AsyncKick')) { 86 \Smartbuy\SMBCommerce\Core\AsyncKick::init(); 87 } 80 88 81 89 if (class_exists('\\Smartbuy\SMBCommerce\\Core\\CronManager')) { … … 90 98 \Smartbuy\SMBCommerce\Analytics\OrderAttribution::init(); 91 99 } 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 } 92 110 } 93 111 } -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/PremiumFallbacks.php
r3747521 r3765552 30 30 'Smartbuy\SMBCommerce\\Commerce\\Woo\\VariationService' => NullVariationService::class, 31 31 'Smartbuy\SMBCommerce\\Commerce\\Woo\\CouponService' => NullCouponService::class, 32 'Smartbuy\SMBCommerce\\Commerce\\Woo\\CartActionService' => NullCartActionService::class, 32 33 '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, 33 37 ]; 34 38 } … … 127 131 } 128 132 133 /** Bản Free: không có hành động giỏ nâng cao (gói Starter); thẻ giữ nút "Choose options". */ 134 class 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. */ 148 class 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). */ 166 class 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). */ 175 class 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 129 182 /** Bản Free: không có thông báo Telegram (gói Starter); lead vẫn được lưu bình thường. */ 130 183 class NullTelegramService { -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Core/Uninstaller.php
r3752682 r3765552 54 54 wp_clear_scheduled_hook('smbcommerce_run_jobs'); 55 55 wp_clear_scheduled_hook('smbcommerce_finalize_outcomes'); 56 \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule(); 56 57 57 58 /* … … 77 78 delete_option('smbcommerce_measurement_started_at'); 78 79 delete_option('smbcommerce_outcomes_watermark'); 80 delete_option('smbcommerce_ai_max_concurrency'); 81 delete_option('smbcommerce_async_mode'); 82 delete_option('smbcommerce_async_health'); 79 83 delete_transient('smbcommerce_missing_key_notice'); 84 delete_option('smbcommerce_review_prompt'); 85 delete_transient('smbcommerce_wp_ai_available'); 80 86 } 81 87 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Licensing/LicenseGate.php
r3752682 r3765552 45 45 // endpoint chỉ trả trường doanh thu khi can('revenue_attribution'). 46 46 '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, 47 53 48 54 // --- Pro --- -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Privacy/PersonalData.php
r3752682 r3765552 185 185 } 186 186 187 $E = '\\Smartbuy\\SMBCommerce\\Analytics\\ValueEvents'; 187 188 $events = $wpdb->get_results($wpdb->prepare( 188 189 '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')) . ') 190 191 ORDER BY id ASC', 191 192 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], 193 196 $conversation_ids 194 197 ) … … 198 201 foreach ($events as $event) { 199 202 $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 } 201 224 $items[] = [ 202 225 'group_id' => 'smb-commerce-value', … … 205 228 'data' => [ 206 229 ['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, 210 231 ['name' => __('Recorded (UTC)', 'ninhle-ai-sales-assistant'), 'value' => $event['created_at']], 211 232 ], -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/AdminController.php
r3761175 r3765552 109 109 } 110 110 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 111 139 public static function delete_conversation($request) { 112 140 if (!current_user_can('manage_smbcommerce')) { … … 170 198 $settings['smbcommerce_font_family'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_font_family', 'system'); 171 199 $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); 172 205 173 206 $settings['smbcommerce_openai_key_exists'] = !empty(\Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_openai_key')); … … 232 265 $settings['smbcommerce_hide_on_checkout'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_hide_on_checkout', '0'); 233 266 $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 } 234 274 235 275 return rest_ensure_response(['settings' => $settings]); … … 280 320 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_font_family', sanitize_text_field($params['smbcommerce_font_family'])); 281 321 } 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']); 284 326 } 285 327 … … 397 439 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_telegram_link', sanitize_text_field($params['smbcommerce_telegram_link'])); 398 440 } 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) { 400 442 if (isset($params[$toggle])) { 401 443 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting($toggle, (string) $params[$toggle] === '1' ? '1' : '0'); … … 405 447 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_notice_text', sanitize_text_field($params['smbcommerce_ai_notice_text'])); 406 448 } 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(); 407 475 408 476 return rest_ensure_response(['success' => true]); -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/ChatController.php
r3752682 r3765552 13 13 14 14 class 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 16 19 public static function bootstrap($request) { 17 20 $session_token = $request->get_header('X-SMB-Commerce-Session'); … … 40 43 $response['status'] = $conversation['status']; 41 44 $response['agent_name'] = self::agent_name($conversation); 45 $response['language'] = $conversation['language'] ?? null; 42 46 43 47 $messages = MessageRepository::get_messages($conversation['id'], 0, 50); … … 108 112 } 109 113 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 110 120 // 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. 111 121 \Smartbuy\SMBCommerce\Analytics\ValueEvents::record( … … 121 131 $is_thinking_mode = rest_sanitize_boolean($request->get_param('thinking_mode')); 122 132 123 \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE,[133 $payload = [ 124 134 'conversation_id' => (int) $conversation_id, 125 135 '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 } 127 161 128 162 return rest_ensure_response($message); … … 145 179 } 146 180 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); 150 187 151 188 $messages = MessageRepository::get_messages($conversation_id, $last_message_id, 50); 152 189 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 } 155 203 156 204 return rest_ensure_response([ … … 160 208 'status' => $conversation['status'], 161 209 '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, 163 213 ]); 164 214 } … … 236 286 237 287 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 }); 239 291 240 292 \Smartbuy\SMBCommerce\Webhooks\WebhookDispatcher::send( … … 273 325 274 326 /** 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']; 276 328 277 329 /** … … 310 362 ['answer' => $type === 'resolution_yes' ? 'yes' : 'no'] 311 363 ); 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 } 312 368 return rest_ensure_response(['recorded' => $recorded]); 313 369 } … … 318 374 } 319 375 $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 320 390 $recorded = \Smartbuy\SMBCommerce\Analytics\ValueEvents::record( 321 391 \Smartbuy\SMBCommerce\Analytics\ValueEvents::QUALIFYING_INTERACTION, -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/RouteRegistry.php
r3752682 r3765552 9 9 10 10 class RouteRegistry { 11 /** @var float|null Lúc handler của plugin bắt đầu, cho header Server-Timing. */ 12 private static $started = null; 13 11 14 public static function init() { 12 15 add_action('rest_api_init', [self::class, 'register_routes']); 16 add_filter('rest_request_before_callbacks', [self::class, 'start_timer'], 10, 3); 13 17 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; 14 25 } 15 26 … … 22 33 */ 23 34 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)) { 25 40 return $response; 26 41 } … … 32 47 // 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ọ. 33 48 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 } 34 57 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); 35 68 } 36 69 … … 70 103 ]); 71 104 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 72 118 register_rest_route($namespace, '/chat/clear', [ 73 119 'methods' => \WP_REST_Server::CREATABLE, … … 98 144 'type' => Guard::arg_text(true, 32), 99 145 '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], 100 148 ], 101 149 ]); … … 263 311 ]); 264 312 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 265 339 // Bảng "AI Value & Performance" (mọi gói; doanh thu kiểm quyền trong handler). 266 340 register_rest_route($namespace, '/admin/value-metrics', [ … … 317 391 '/tools/product-variations' => 'get_product_variations', 318 392 '/tools/check-coupon' => 'check_coupon', 393 '/tools/propose-cart-action' => 'propose_cart_action', 319 394 '/tools/order-status' => 'get_order_status', 395 '/tools/my-orders' => 'get_my_orders', 320 396 '/tools/customer-downloads' => 'get_customer_downloads', 321 397 '/tools/policy' => 'get_policy', -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/REST/ToolController.php
r3747521 r3765552 59 59 } 60 60 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 61 83 public static function get_product_detail($request) { 62 84 $service = new \Smartbuy\SMBCommerce\Commerce\Woo\ProductService(); … … 87 109 ]; 88 110 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; 92 116 } 93 117 … … 95 119 96 120 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)); 97 131 } 98 132 -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/Widget/WidgetLoader.php
r3761175 r3765552 63 63 'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')), 64 64 '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(), 65 68 'widgetConfig' => self::public_config(), 66 69 // Dịch ở phía PHP để chuỗi vào được file .pot và theo locale WordPress. 67 70 'i18n' => [ 68 71 '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'), 69 74 'chatWindow' => __('Chat with our assistant', 'ninhle-ai-sales-assistant'), 70 75 'cart' => __('Cart', 'ninhle-ai-sales-assistant'), … … 109 114 'stageOrder' => __('Looking up your order…', 'ninhle-ai-sales-assistant'), 110 115 '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'), 111 154 'resolutionQuestion' => __('Did this answer your question?', 'ninhle-ai-sales-assistant'), 112 155 'yes' => __('Yes', 'ninhle-ai-sales-assistant'), … … 125 168 'voiceLanguage' => __('Your browser cannot recognise this language. Please type your message.', 'ninhle-ai-sales-assistant'), 126 169 '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(), 128 172 ]; 129 173 … … 160 204 // Giỏ hàng chỉ có khi WooCommerce đang chạy. 161 205 '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(), 162 213 // Thông báo "đang trò chuyện với AI": mặc định bật; rỗng = tắt. 163 214 'aiNotice' => $get('smbcommerce_ai_notice_enabled', '1') === '1' -
ninhle-ai-sales-assistant/tags/2.6.0/src/PHP/WooCommerce/WooCommerceProductScanner.php
r3761175 r3765552 34 34 35 35 $synced_ids = []; 36 $fetched = 0; 36 37 $page = 1; 37 38 … … 52 53 53 54 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 } 54 61 $this->upsert_product($product, $table, $now); 55 62 $synced_ids[] = $product->get_id(); 56 63 } 57 64 65 $fetched += count($products); 58 66 $remaining -= count($products); 59 67 $page++; … … 64 72 } 65 73 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]; 68 78 $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) 71 81 )); 72 82 } … … 127 137 128 138 $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() : ''; 130 143 // 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 131 144 // 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. … … 215 228 $p['name'], 216 229 (!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']) . ')' : '', 218 231 $this->describe_stock($p['stock_status'], $p['stock_quantity'] ?? null), 219 232 $p['categories'] ?: 'Chưa phân loại', … … 227 240 $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"; 228 241 242 $this->context_ids = array_map(fn($p) => (int) $p['id'], $products); 229 243 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; 230 252 } 231 253 … … 252 274 AND (%s = '' OR search_text LIKE %s OR title LIKE %s OR sku LIKE %s) 253 275 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) 256 278 ORDER BY relevance DESC, total_sales DESC, cache_synced_at DESC 257 279 LIMIT %d", … … 274 296 ), ARRAY_A); 275 297 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 276 304 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) ?: []; 277 354 } 278 355 … … 313 390 'price' => $row['price'], 314 391 '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'], 316 394 // 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 317 395 // cho một size/màu cụ thể. -
ninhle-ai-sales-assistant/tags/2.6.0/vite.config.ts
r3761175 r3765552 4 4 import { resolve } from 'path'; 5 5 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) 9 11 // Chạy: npm ci && npm run build → assets/dist/ 10 12 // build.ps1 chép assets/dist/.vite/manifest.json ra assets/dist/manifest.json khi đóng gói. … … 14 16 plugins: [ 15 17 preact({ 16 include: /widget \/.*\.tsx?$/,18 include: /widget(-pro)?\/.*\.tsx?$/, 17 19 }), 18 20 react({ … … 30 32 manifest: true, 31 33 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', 32 37 input: { 33 38 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') 35 43 }, 36 44 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:1 import{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: 2 2 Help me choose a plan 3 3 Do 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,{})); 4 What 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 5 FREESHIP`,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-c ommerce-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 1 1 { 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 } 57 116 } -
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);1 const __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]); 2 import{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 3 3 msgid "" 4 4 msgstr "" 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" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ninhle-ai-sales-assistant\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-10-2 3T06:16:45+00:00\n"12 "POT-Creation-Date: 2026-10-26T03:45:21+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 17 17 #. Plugin Name of the plugin 18 18 #: ninhle-ai-sales-assistant.php 19 msgid "Ninhle AI Sales Assistant for WooCommerce"19 msgid "Ninhle AI Sales Assistant – AI Chatbot for WooCommerce" 20 20 msgstr "" 21 21 22 22 #. Description of the plugin 23 23 #: 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."24 msgid "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." 25 25 msgstr "" 26 26 … … 35 35 msgstr "" 36 36 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 40 msgid "%s%% off" 41 msgstr "" 42 43 #. translators: %s: discount amount with currency. 44 #: premium/Commerce/Woo/CartActionService.php:381 45 #, php-format 46 msgid "%s off each item" 47 msgstr "" 48 49 #. translators: %s: discount amount with currency. 50 #: premium/Commerce/Woo/CartActionService.php:384 51 #, php-format 52 msgid "%s off your order" 53 msgstr "" 54 55 #: premium/Commerce/Woo/CartActionService.php:471 56 msgid "Quantity" 57 msgstr "" 58 59 #: premium/Commerce/Woo/CartActionService.php:472 60 msgid "Decrease quantity" 61 msgstr "" 62 63 #: premium/Commerce/Woo/CartActionService.php:473 64 msgid "Increase quantity" 65 msgstr "" 66 67 #. translators: %s: product name. 68 #: premium/Commerce/Woo/CartActionService.php:475 69 #, php-format 70 msgid "Remove %s" 71 msgstr "" 72 73 #: premium/Commerce/Woo/CartActionService.php:476 74 msgid "Remove" 75 msgstr "" 76 77 #: premium/Commerce/Woo/CartActionService.php:477 78 msgid "Choose your options to add this to the cart." 79 msgstr "" 80 81 #: premium/Commerce/Woo/CartActionService.php:478 82 msgid "sold out" 83 msgstr "" 84 85 #: premium/Commerce/Woo/CartActionService.php:479 86 msgid "This combination is not available." 87 msgstr "" 88 89 #. translators: %d: largest quantity that can be added. 90 #: premium/Commerce/Woo/CartActionService.php:481 91 #, php-format 92 msgid "Up to %d" 93 msgstr "" 94 95 #: premium/Commerce/Woo/CartActionService.php:482 96 msgid "Updating your cart…" 97 msgstr "" 98 99 #: premium/Commerce/Woo/CartActionService.php:483 100 msgid "Couldn't update your cart." 101 msgstr "" 102 103 #: premium/Commerce/Woo/CartActionService.php:484 104 msgid "Apply" 105 msgstr "" 106 107 #: premium/Commerce/Woo/CartActionService.php:485 108 msgid "Applying…" 109 msgstr "" 110 111 #. translators: %s: coupon code. 112 #: premium/Commerce/Woo/CartActionService.php:487 113 #, php-format 114 msgid "Code %s applied" 115 msgstr "" 116 117 #: premium/Commerce/Woo/CartActionService.php:488 118 msgid "Couldn't apply this code." 119 msgstr "" 120 121 #. translators: %s: minimum order amount with currency. 122 #: premium/Commerce/Woo/CartActionService.php:490 123 #, php-format 124 msgid "Minimum order %s" 125 msgstr "" 126 127 #: premium/Commerce/Woo/CartActionService.php:491 128 msgid "Discount codes" 129 msgstr "" 130 131 #: premium/CRM/CrmStore.php:609 132 msgid "Customer ID" 133 msgstr "" 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 139 msgid "Name" 140 msgstr "" 141 142 #: premium/CRM/CrmStore.php:611 143 msgid "Verified" 144 msgstr "" 145 146 #: premium/CRM/CrmStore.php:611 147 #: src/PHP/Widget/WidgetLoader.php:155 148 msgid "Yes" 149 msgstr "" 150 151 #: premium/CRM/CrmStore.php:611 152 msgid "No" 153 msgstr "" 154 155 #. translators: %s: contact type, for example "email" or "phone". 156 #: premium/CRM/CrmStore.php:616 157 #, php-format 158 msgid "Contact (%s)" 159 msgstr "" 160 161 #: premium/CRM/CrmStore.php:620 162 msgid "Ninhle AI Sales Assistant customer profile" 163 msgstr "" 164 165 #: premium/CRM/CrmStore.php:623 166 msgid "Ninhle AI Sales Assistant contact permissions" 167 msgstr "" 168 169 #: premium/CRM/CrmStore.php:624 170 msgid "Purpose" 171 msgstr "" 172 173 #: premium/CRM/CrmStore.php:625 174 #: premium/CRM/CrmStore.php:666 175 #: src/PHP/Privacy/PersonalData.php:296 176 msgid "Channel" 177 msgstr "" 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 183 msgid "Status" 184 msgstr "" 185 186 #: premium/CRM/CrmStore.php:627 187 msgid "Source" 188 msgstr "" 189 190 #: premium/CRM/CrmStore.php:628 191 msgid "Recorded" 192 msgstr "" 193 194 #: premium/CRM/CrmStore.php:634 195 msgid "Ninhle AI Sales Assistant purchase opportunities" 196 msgstr "" 197 198 #: premium/CRM/CrmStore.php:635 199 msgid "Need" 200 msgstr "" 201 202 #: premium/CRM/CrmStore.php:636 203 msgid "Stage" 204 msgstr "" 205 206 #: premium/CRM/CrmStore.php:637 207 msgid "Payment" 208 msgstr "" 209 210 #: premium/CRM/CrmStore.php:638 211 #: src/PHP/Widget/WidgetLoader.php:118 212 msgid "Products" 213 msgstr "" 214 215 #: premium/CRM/CrmStore.php:639 216 #: src/PHP/Admin/AdminMenu.php:215 217 msgid "Orders" 218 msgstr "" 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 224 msgid "Created" 225 msgstr "" 226 227 #: premium/CRM/CrmStore.php:664 228 msgid "Ninhle AI Sales Assistant customer history" 229 msgstr "" 230 231 #: premium/CRM/CrmStore.php:665 232 #: premium/CRM/CrmStore.php:675 233 msgid "Type" 234 msgstr "" 235 236 #: premium/CRM/CrmStore.php:667 237 msgid "Content" 238 msgstr "" 239 240 #: premium/CRM/CrmStore.php:668 241 #: src/PHP/Privacy/PersonalData.php:337 242 msgid "When" 243 msgstr "" 244 245 #: premium/CRM/CrmStore.php:674 246 msgid "Ninhle AI Sales Assistant support and follow-up requests" 247 msgstr "" 248 249 #: premium/CRM/CrmStore.php:677 250 msgid "Resolution" 251 msgstr "" 252 253 #. translators: %d: number of chats 254 #: premium/Insights/InsightsDigest.php:66 255 #, php-format 256 msgid "%d chat with the assistant in the last 7 days." 257 msgid_plural "%d chats with the assistant in the last 7 days." 258 msgstr[0] "" 259 msgstr[1] "" 260 261 #: premium/Insights/InsightsDigest.php:69 262 msgid "Questions the assistant could not answer:" 263 msgstr "" 264 265 #: premium/Insights/InsightsDigest.php:72 266 #: premium/Insights/InsightsDigest.php:94 267 msgid "None this week." 268 msgstr "" 269 270 #. translators: 1: customer question, 2: number of times asked 271 #: premium/Insights/InsightsDigest.php:76 272 #, php-format 273 msgid "- \"%1$s\" (%2$d×)" 274 msgstr "" 275 276 #: premium/Insights/InsightsDigest.php:80 277 msgid "What customers looked for but could not get:" 278 msgstr "" 279 280 #. translators: 1: product name, 2: number of times asked 281 #: premium/Insights/InsightsDigest.php:84 282 #, php-format 283 msgid "- %1$s: out of stock (%2$d×)" 284 msgstr "" 285 286 #. translators: 1: search words, 2: number of searches 287 #: premium/Insights/InsightsDigest.php:88 288 #, php-format 289 msgid "- \"%1$s\": no matching product (%2$d×)" 290 msgstr "" 291 292 #. translators: 1: reason, 2: percentage, 3: number of conversations labeled 293 #: premium/Insights/InsightsDigest.php:104 294 #, php-format 295 msgid "Most common reason shoppers did not buy: %1$s (%2$s%% of %3$d chats)." 296 msgstr "" 297 298 #. translators: %s: amount in US dollars 299 #: premium/Insights/InsightsDigest.php:110 300 #, php-format 301 msgid "AI cost in the last 7 days: %s" 302 msgstr "" 303 304 #. translators: %s: link to the Sales Insights tab 305 #: premium/Insights/InsightsDigest.php:113 306 #, php-format 307 msgid "Open Sales Insights: %s" 308 msgstr "" 309 310 #: premium/Insights/InsightsDigest.php:114 311 msgid "You can turn this email off in Sales Insights > Settings." 312 msgstr "" 313 314 #. translators: %s: site name 315 #: premium/Insights/InsightsDigest.php:118 316 #, php-format 317 msgid "[%s] Your weekly Sales Insights" 318 msgstr "" 319 320 #: premium/Insights/InsightsDigest.php:125 321 msgid "price" 322 msgstr "" 323 324 #: premium/Insights/InsightsDigest.php:126 325 msgid "shipping cost or time" 326 msgstr "" 327 328 #: premium/Insights/InsightsDigest.php:127 329 msgid "unsure about size or fit" 330 msgstr "" 331 332 #: premium/Insights/InsightsDigest.php:128 333 msgid "out of stock" 334 msgstr "" 335 336 #: premium/Insights/InsightsDigest.php:129 337 msgid "missing information" 338 msgstr "" 339 340 #: premium/Insights/InsightsDigest.php:130 341 msgid "waiting for a discount" 342 msgstr "" 343 344 #: premium/Insights/InsightsDigest.php:131 345 msgid "just browsing" 346 msgstr "" 347 348 #: premium/Insights/InsightsDigest.php:132 349 msgid "other" 350 msgstr "" 351 352 #: premium/Integrations/TelegramService.php:77 353 msgid "None" 354 msgstr "" 355 356 #. translators: %s: chat widget brand name. 357 #: premium/Integrations/TelegramService.php:79 358 #, php-format 359 msgid "New lead from %s" 360 msgstr "" 361 362 #: premium/Integrations/TelegramService.php:80 363 msgid "Name:" 364 msgstr "" 365 366 #: premium/Integrations/TelegramService.php:81 367 msgid "Email:" 368 msgstr "" 369 370 #: premium/Integrations/TelegramService.php:82 371 msgid "Phone:" 372 msgstr "" 373 374 #: premium/REST/TelegramController.php:55 375 #: src/PHP/Core/JobDispatcher.php:99 376 msgid "Sorry, the assistant is temporarily unavailable. Please try again shortly." 377 msgstr "" 378 379 #: premium/Webhooks/WebhookDispatcher.php:113 380 msgid "Outgoing webhooks are available in the Pro plan." 381 msgstr "" 382 383 #: src/PHP/Abilities/AbilitiesService.php:57 384 msgid "Store catalog" 385 msgstr "" 386 387 #: src/PHP/Abilities/AbilitiesService.php:58 388 msgid "Read-only access to the public product catalog and store policies." 389 msgstr "" 390 391 #: src/PHP/Abilities/AbilitiesService.php:75 392 msgid "Search products" 393 msgstr "" 394 395 #: src/PHP/Abilities/AbilitiesService.php:76 396 msgid "Searches the public product catalog of this store. Returns up to 10 products with price, currency, stock status, link and image." 397 msgstr "" 398 399 #: src/PHP/Abilities/AbilitiesService.php:104 400 msgid "Get product details" 401 msgstr "" 402 403 #: src/PHP/Abilities/AbilitiesService.php:105 404 msgid "Returns public details of one product: price, stock, description, categories, attributes and, for products with options, each option with its price and stock." 405 msgstr "" 406 407 #: src/PHP/Abilities/AbilitiesService.php:120 408 msgid "Get store policies" 409 msgstr "" 410 411 #: src/PHP/Abilities/AbilitiesService.php:121 412 msgid "Returns the store delivery, refund and warranty policies and the payment methods it accepts." 413 msgstr "" 414 415 #: src/PHP/Abilities/AbilitiesService.php:153 416 msgid "Too many requests. Try again later." 417 msgstr "" 418 419 #: src/PHP/Abilities/AbilitiesService.php:197 420 msgid "Product not found." 421 msgstr "" 422 38 423 #: src/PHP/Admin/AdminMenu.php:26 39 424 msgid "Ninhle AI Sales Assistant" 40 425 msgstr "" 41 426 42 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:2743 427 #: src/PHP/Admin/AdminMenu.php:27 44 428 msgid "Ninhle AI Sales" 45 429 msgstr "" 46 430 47 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/AdminMenu.php:9148 431 #: src/PHP/Admin/AdminMenu.php:91 432 msgid "Sales Insights" 433 msgstr "" 434 435 #: src/PHP/Admin/AdminMenu.php:92 436 msgid "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." 437 msgstr "" 438 439 #. translators: %s: number of chats 440 #: src/PHP/Admin/AdminMenu.php:94 441 #, php-format 442 msgid "%s chats in this period" 443 msgstr "" 444 445 #. translators: 1: number of chats so far, 2: number of chats needed 446 #: src/PHP/Admin/AdminMenu.php:96 447 #, php-format 448 msgid "Not enough data yet (%1$s of %2$s chats); the lists below are a first look." 449 msgstr "" 450 451 #: src/PHP/Admin/AdminMenu.php:98 452 msgid "Questions the assistant could not answer" 453 msgstr "" 454 455 #: src/PHP/Admin/AdminMenu.php:99 456 msgid "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." 457 msgstr "" 458 459 #: src/PHP/Admin/AdminMenu.php:100 460 msgid "No unanswered questions in this period." 461 msgstr "" 462 463 #: src/PHP/Admin/AdminMenu.php:101 464 #: src/PHP/Admin/AdminMenu.php:116 465 msgid "Question" 466 msgstr "" 467 468 #: src/PHP/Admin/AdminMenu.php:102 469 msgid "Times asked" 470 msgstr "" 471 472 #: src/PHP/Admin/AdminMenu.php:103 473 msgid "Last asked" 474 msgstr "" 475 476 #: src/PHP/Admin/AdminMenu.php:104 477 msgid "Actions" 478 msgstr "" 479 480 #. translators: %s: other ways the question was asked 481 #: src/PHP/Admin/AdminMenu.php:106 482 #, php-format 483 msgid "Also asked as: %s" 484 msgstr "" 485 486 #: src/PHP/Admin/AdminMenu.php:107 487 msgid "shopper said it was not answered" 488 msgstr "" 489 490 #: src/PHP/Admin/AdminMenu.php:108 491 msgid "information missing" 492 msgstr "" 493 494 #: src/PHP/Admin/AdminMenu.php:109 495 msgid "no matching product" 496 msgstr "" 497 498 #: src/PHP/Admin/AdminMenu.php:110 499 msgid "passed to your team" 500 msgstr "" 501 502 #: src/PHP/Admin/AdminMenu.php:111 503 msgid "local-only mode" 504 msgstr "" 505 506 #: src/PHP/Admin/AdminMenu.php:112 507 msgid "Add answer" 508 msgstr "" 509 510 #: src/PHP/Admin/AdminMenu.php:113 511 msgid "Dismiss" 512 msgstr "" 513 514 #: src/PHP/Admin/AdminMenu.php:114 515 msgid "Question dismissed." 516 msgstr "" 517 518 #: src/PHP/Admin/AdminMenu.php:115 519 msgid "Open a chat" 520 msgstr "" 521 522 #: src/PHP/Admin/AdminMenu.php:117 523 msgid "Answer" 524 msgstr "" 525 526 #: src/PHP/Admin/AdminMenu.php:118 527 msgid "Where to save the answer" 528 msgstr "" 529 530 #: src/PHP/Admin/AdminMenu.php:119 531 msgid "Add to Business information (the assistant uses it from the next reply)" 532 msgstr "" 533 534 #: src/PHP/Admin/AdminMenu.php:120 535 msgid "Save as a note so I can update my policy pages" 536 msgstr "" 537 538 #: src/PHP/Admin/AdminMenu.php:121 539 msgid "Save answer" 540 msgstr "" 541 542 #: src/PHP/Admin/AdminMenu.php:122 543 #: src/PHP/Widget/WidgetLoader.php:81 544 msgid "Cancel" 545 msgstr "" 546 547 #: src/PHP/Admin/AdminMenu.php:123 548 msgid "Answer added to Business information." 549 msgstr "" 550 551 #: src/PHP/Admin/AdminMenu.php:124 552 msgid "Answer saved as a note." 553 msgstr "" 554 555 #: src/PHP/Admin/AdminMenu.php:125 556 msgid "Could not save the answer. Enter both the question and the answer." 557 msgstr "" 558 559 #: src/PHP/Admin/AdminMenu.php:126 560 msgid "Notes for your policy pages" 561 msgstr "" 562 563 #: src/PHP/Admin/AdminMenu.php:127 564 msgid "Answers you chose to add to your own pages. The assistant does not read these notes." 565 msgstr "" 566 567 #: src/PHP/Admin/AdminMenu.php:128 568 msgid "Delete" 569 msgstr "" 570 571 #: src/PHP/Admin/AdminMenu.php:130 572 msgid "What shoppers could not find" 573 msgstr "" 574 575 #: src/PHP/Admin/AdminMenu.php:131 576 msgid "Searches with no matching product" 577 msgstr "" 578 579 #: src/PHP/Admin/AdminMenu.php:132 580 msgid "Wanted but out of stock" 581 msgstr "" 582 583 #: src/PHP/Admin/AdminMenu.php:133 584 msgid "None in this period." 585 msgstr "" 586 587 #: src/PHP/Admin/AdminMenu.php:134 588 msgid "Search your products" 589 msgstr "" 590 591 #: src/PHP/Admin/AdminMenu.php:135 592 msgid "In stock now" 593 msgstr "" 594 595 #: src/PHP/Admin/AdminMenu.php:136 596 msgid "Backorders allowed" 597 msgstr "" 598 599 #: src/PHP/Admin/AdminMenu.php:137 600 msgid "Restock it, or allow backorders under Product data > Inventory." 601 msgstr "" 602 603 #: src/PHP/Admin/AdminMenu.php:139 604 msgid "Why shoppers did not buy" 605 msgstr "" 606 607 #: src/PHP/Admin/AdminMenu.php:140 608 msgid "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." 609 msgstr "" 610 611 #: src/PHP/Admin/AdminMenu.php:141 612 msgid "Turn on AI labeling below to see why shoppers did not buy." 613 msgstr "" 614 615 #: src/PHP/Admin/AdminMenu.php:142 616 msgid "AI labeling is off: these are earlier results. New chats are not labeled until you turn it back on." 617 msgstr "" 618 619 #. translators: 1: chats labeled so far, 2: chats needed 620 #: src/PHP/Admin/AdminMenu.php:144 621 #, php-format 622 msgid "Not enough data yet: %1$s of %2$s chats labeled. Shares appear once there are enough." 623 msgstr "" 624 625 #: src/PHP/Admin/AdminMenu.php:145 626 msgid "Price" 627 msgstr "" 628 629 #: src/PHP/Admin/AdminMenu.php:146 630 msgid "Shipping cost or time" 631 msgstr "" 632 633 #: src/PHP/Admin/AdminMenu.php:147 634 msgid "Unsure about size or fit" 635 msgstr "" 636 637 #: src/PHP/Admin/AdminMenu.php:148 638 #: src/PHP/Widget/WidgetLoader.php:123 639 msgid "Out of stock" 640 msgstr "" 641 642 #: src/PHP/Admin/AdminMenu.php:149 643 msgid "Missing information" 644 msgstr "" 645 646 #: src/PHP/Admin/AdminMenu.php:150 647 msgid "Waiting for a discount" 648 msgstr "" 649 650 #: src/PHP/Admin/AdminMenu.php:151 651 msgid "Just browsing" 652 msgstr "" 653 654 #: src/PHP/Admin/AdminMenu.php:152 655 msgid "Other" 656 msgstr "" 657 658 #: src/PHP/Admin/AdminMenu.php:153 659 msgid "See the unanswered questions" 660 msgstr "" 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 665 msgid "Model: %1$s · This month: %2$s of %3$s" 666 msgstr "" 667 668 #. translators: %s: date 669 #: src/PHP/Admin/AdminMenu.php:157 670 #, php-format 671 msgid "Last run: %s" 672 msgstr "" 673 674 #: src/PHP/Admin/AdminMenu.php:158 675 msgid "Stopped: the monthly limit for AI labeling was reached." 676 msgstr "" 677 678 #: src/PHP/Admin/AdminMenu.php:159 679 msgid "Stopped: the overall AI budget was reached." 680 msgstr "" 681 682 #: src/PHP/Admin/AdminMenu.php:160 683 msgid "Stopped: no AI provider with an API key is set up." 684 msgstr "" 685 686 #: src/PHP/Admin/AdminMenu.php:161 687 msgid "Paused: 200 chats are labeled per night at most." 688 msgstr "" 689 690 #: src/PHP/Admin/AdminMenu.php:163 691 msgid "Sales Insights settings" 692 msgstr "" 693 694 #: src/PHP/Admin/AdminMenu.php:164 695 msgid "Label why shoppers did not buy, each night with AI" 696 msgstr "" 697 698 #: src/PHP/Admin/AdminMenu.php:165 699 msgid "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." 700 msgstr "" 701 702 #: src/PHP/Admin/AdminMenu.php:166 703 msgid "Monthly limit (USD)" 704 msgstr "" 705 706 #: src/PHP/Admin/AdminMenu.php:167 707 msgid "Model" 708 msgstr "" 709 710 #: src/PHP/Admin/AdminMenu.php:168 711 msgid "Cheapest available (recommended)" 712 msgstr "" 713 714 #: src/PHP/Admin/AdminMenu.php:169 715 msgid "Same as the chat assistant" 716 msgstr "" 717 718 #: src/PHP/Admin/AdminMenu.php:170 719 msgid "Email me a weekly summary" 720 msgstr "" 721 722 #: src/PHP/Admin/AdminMenu.php:171 723 msgid "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." 724 msgstr "" 725 726 #: src/PHP/Admin/AdminMenu.php:178 49 727 msgid "AI Value & Performance" 50 728 msgstr "" 51 729 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 54 731 msgid "What the assistant cost, the orders it helped with and how conversations ended. Figures start on the measurement start date and are never backfilled." 55 732 msgstr "" 56 733 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 59 735 msgid "Report period" 60 736 msgstr "" 61 737 62 738 #. 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 65 740 #, php-format 66 741 msgid "Last %s days" … … 68 743 69 744 #. 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 72 746 #, php-format 73 747 msgid "Measured since %s" 74 748 msgstr "" 75 749 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 78 751 msgid "Measurement starts after the database update to version 2.4.0." 79 752 msgstr "" 80 753 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 83 755 msgid "Loading the figures…" 84 756 msgstr "" 85 757 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 88 759 msgid "Could not load the figures. Please reload the page." 89 760 msgstr "" 90 761 91 762 #. 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 94 764 #, php-format 95 765 msgid "n = %s" 96 766 msgstr "" 97 767 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 100 769 msgid "Cost and estimated comparison" 101 770 msgstr "" 102 771 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 105 773 msgid "AI cost recorded" 106 774 msgstr "" 107 775 108 776 #. 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 111 778 #, php-format 112 779 msgid "%1$s from provider usage, %2$s estimated" … … 114 781 115 782 #. 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 118 784 #, php-format 119 785 msgid "%s requests used a model with no known price and are counted as $0." 120 786 msgstr "" 121 787 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 124 789 msgid "Plugin cost, allocated per day" 125 790 msgstr "" 126 791 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 129 793 msgid "Other costs you entered" 130 794 msgstr "" 131 795 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 134 797 msgid "Total" 135 798 msgstr "" 136 799 137 800 #. 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 140 802 #, php-format 141 803 msgid "Reference option: %s" … … 143 805 144 806 #. 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 147 808 #, php-format 148 809 msgid "%1$s × %2$s" 149 810 msgstr "" 150 811 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 153 813 msgid "Estimated difference" 154 814 msgstr "" 155 815 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 158 817 msgid "Estimate based on the reference option you chose; not a verified saving." 159 818 msgstr "" 160 819 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 163 821 msgid "Add a reference option in Measurement settings to compare costs." 164 822 msgstr "" 165 823 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 168 825 msgid "AI cost covers model usage only. It is not the total cost of ownership." 169 826 msgstr "" 170 827 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 173 829 msgid "per AI conversation" 174 830 msgstr "" 175 831 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 178 833 msgid "per conversation the customer confirmed as resolved" 179 834 msgstr "" 180 835 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 183 837 msgid "per month" 184 838 msgstr "" 185 839 186 840 #. 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 189 842 #, php-format 190 843 msgid "Source: %s" 191 844 msgstr "" 192 845 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 195 847 msgid "AI-assisted orders" 196 848 msgstr "" 197 849 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 206 851 msgid "AI-assisted does not mean the AI was the only reason for the order." 207 852 msgstr "" 208 853 209 854 #. 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 212 856 #, php-format 213 857 msgid "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." 214 858 msgstr "" 215 859 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 218 861 msgid "AI-assisted revenue" 219 862 msgstr "" 220 863 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 223 865 msgid "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." 224 866 msgstr "" 225 867 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 228 869 msgid "Revenue per currency is included in the Starter and Pro plans." 229 870 msgstr "" 230 871 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 233 873 msgid "No AI-assisted revenue in this period." 234 874 msgstr "" 235 875 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 238 877 msgid "Conversation outcomes" 239 878 msgstr "" 240 879 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 243 881 msgid "Finished conversations" 244 882 msgstr "" 245 883 246 884 #. 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 249 886 #, php-format 250 887 msgid "Not enough data yet: %1$s of %2$s finished conversations needed before rates are shown." 251 888 msgstr "" 252 889 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 255 891 msgid "Resolved, confirmed by the customer" 256 892 msgstr "" 257 893 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 260 895 msgid "Probably resolved: the customer clicked a product, added to cart or ordered" 261 896 msgstr "" 262 897 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 265 899 msgid "Handed off to staff" 266 900 msgstr "" 267 901 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 270 903 msgid "Unresolved, left before an answer, or unknown" 271 904 msgstr "" 272 905 273 906 #. 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 276 908 #, php-format 277 909 msgid "95%% range: %1$s–%2$s%%" 278 910 msgstr "" 279 911 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 282 913 msgid "\"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." 283 914 msgstr "" 284 915 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 287 917 msgid "Handoffs by source" 288 918 msgstr "" 289 919 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 292 921 msgid "AI" 293 922 msgstr "" 294 923 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 297 925 msgid "Automation rule" 298 926 msgstr "" 299 927 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 302 929 msgid "Customer button" 303 930 msgstr "" 304 931 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 307 933 msgid "Staff took over" 308 934 msgstr "" 309 935 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 312 937 msgid "AI budget reached" 313 938 msgstr "" 314 939 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 317 941 msgid "Measurement settings" 318 942 msgstr "" 319 943 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 322 945 msgid "Reference option name" 323 946 msgstr "" 324 947 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 327 949 msgid "Pricing unit" 328 950 msgstr "" 329 951 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 332 953 msgid "Price (USD)" 333 954 msgstr "" 334 955 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 337 957 msgid "Where the price comes from" 338 958 msgstr "" 339 959 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 342 961 msgid "Price date" 343 962 msgstr "" 344 963 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 347 965 msgid "Enter the price of an alternative you are considering. Leave the price at 0 to show costs only." 348 966 msgstr "" 349 967 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 352 969 msgid "Plugin cost per month (USD)" 353 970 msgstr "" 354 971 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 357 973 msgid "Other costs per month (USD)" 358 974 msgstr "" 359 975 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 362 977 msgid "Attribution window" 363 978 msgstr "" 364 979 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 367 981 msgid "How long after a qualifying chat interaction an order still counts as AI-assisted." 368 982 msgstr "" 369 983 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 372 985 msgid "24 hours" 373 986 msgstr "" 374 987 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 377 989 msgid "72 hours" 378 990 msgstr "" 379 991 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 382 993 msgid "7 days" 383 994 msgstr "" 384 995 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 387 997 msgid "Order statuses that count" 388 998 msgstr "" 389 999 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 392 1001 msgid "Processing" 393 1002 msgstr "" 394 1003 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 397 1005 msgid "Completed" 398 1006 msgstr "" 399 1007 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 402 1009 msgid "On hold" 403 1010 msgstr "" 404 1011 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 407 1013 msgid "Save settings" 408 1014 msgstr "" 409 1015 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 412 1017 msgid "Saving…" 413 1018 msgstr "" 414 1019 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 417 1021 msgid "Measurement settings saved." 418 1022 msgstr "" 419 1023 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 422 1025 msgid "Could not save the measurement settings." 423 1026 msgstr "" 424 1027 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 1031 msgid "In the last 90 days, your AI sales assistant answered %s shopper conversation." 1032 msgid_plural "In the last 90 days, your AI sales assistant answered %s shopper conversations." 1033 msgstr[0] "" 1034 msgstr[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 1039 msgid "%s order was placed after a chat with it." 1040 msgid_plural "%s orders were placed after a chat with it." 1041 msgstr[0] "" 1042 msgstr[1] "" 1043 1044 #: src/PHP/Admin/ReviewPrompt.php:192 1045 msgid "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." 1046 msgstr "" 1047 1048 #: src/PHP/Admin/ReviewPrompt.php:194 1049 msgid "Rate on WordPress.org" 1050 msgstr "" 1051 1052 #: src/PHP/Admin/ReviewPrompt.php:196 1053 msgid "Maybe later" 1054 msgstr "" 1055 1056 #: src/PHP/Admin/ReviewPrompt.php:198 1057 msgid "Don't ask again" 1058 msgstr "" 1059 1060 #: src/PHP/Admin/ReviewPrompt.php:200 1061 msgid "Need help? Ask on the support forum" 1062 msgstr "" 1063 426 1064 #: src/PHP/Admin/SecurityNotice.php:94 427 1065 msgid "Ninhle AI Sales Assistant: rotate your API credentials" 428 1066 msgstr "" 429 1067 430 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:95431 1068 #: src/PHP/Admin/SecurityNotice.php:95 432 1069 msgid "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." 433 1070 msgstr "" 434 1071 435 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:99436 1072 #: src/PHP/Admin/SecurityNotice.php:99 437 1073 msgid "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." 438 1074 msgstr "" 439 1075 440 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SecurityNotice.php:104441 1076 #: src/PHP/Admin/SecurityNotice.php:104 442 1077 msgid "Dismiss this notice" 443 1078 msgstr "" 444 1079 445 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:16446 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:17447 1080 #: src/PHP/Admin/SettingsPage.php:16 448 1081 #: src/PHP/Admin/SettingsPage.php:17 … … 451 1084 452 1085 #. translators: %s: commission rate, for example "30%" 453 #: build/ninhle-ai-sales-assistant/src/PHP/Admin/SettingsPage.php:45454 1086 #: src/PHP/Admin/SettingsPage.php:45 455 1087 #, php-format … … 457 1089 msgstr "" 458 1090 459 #: build/ninhle-ai-sales-assistant/src/PHP/AI/AIService.php:68460 1091 #: src/PHP/AI/AIService.php:68 461 1092 msgid "" … … 465 1096 msgstr "" 466 1097 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 469 1099 msgid "The assistant is running in local-only mode. Please contact us through the channels below for help." 470 1100 msgstr "" 471 1101 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 474 1103 msgid "Hello! The AI assistant is still being set up. Please try again shortly, or contact us through the channels below." 475 1104 msgstr "" 476 1105 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 479 1107 msgid "Sorry, the assistant is temporarily unavailable. Please try again in a moment, or choose \"Talk to a person\"." 480 1108 msgstr "" 481 1109 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 1111 msgid "Sorry, I could not finish looking that up. Please ask me again in a moment." 1112 msgstr "" 1113 1114 #: src/PHP/AI/AIService.php:461 489 1115 msgid "Sorry, I could not put an answer together just now. Could you ask that again in a different way?" 490 1116 msgstr "" 491 1117 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 494 1119 msgid "I understand. Let me bring in a member of our team to help you directly. Please wait a moment." 495 1120 msgstr "" 496 1121 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 499 1123 msgid "We are connecting you with a manager now. Please stay in the chat." 500 1124 msgstr "" 501 1125 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 504 1127 msgid "Your request is being escalated urgently to the responsible team. Please do not send any more personal information here." 505 1128 msgstr "" 506 1129 507 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:69508 1130 #: src/PHP/Commerce/LocalHandler.php:69 509 1131 msgid "Hello! How can I help you today?" … … 511 1133 512 1134 #. translators: %s is the store or brand name. 513 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:74514 1135 #: src/PHP/Commerce/LocalHandler.php:74 515 1136 #, php-format … … 517 1138 msgstr "" 518 1139 519 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:80520 1140 #: src/PHP/Commerce/LocalHandler.php:80 521 1141 msgid "You are welcome. Just message me if you need anything else." 522 1142 msgstr "" 523 1143 524 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:123525 1144 #: src/PHP/Commerce/LocalHandler.php:123 526 1145 msgid "Your cart is empty. Tell me which product you want and I will send you the link." … … 528 1147 529 1148 #. translators: %s is the checkout page URL. 530 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:135531 1149 #: src/PHP/Commerce/LocalHandler.php:135 532 1150 #, php-format … … 535 1153 536 1154 #. translators: %s is the cart page URL. 537 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:155538 1155 #: src/PHP/Commerce/LocalHandler.php:155 539 1156 #, php-format … … 541 1158 msgstr "" 542 1159 543 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:163544 1160 #: src/PHP/Commerce/LocalHandler.php:163 1161 #: src/PHP/Widget/WidgetLoader.php:142 545 1162 msgid "Your cart is empty." 546 1163 msgstr "" 547 1164 548 1165 #. translators: 1: list of cart lines, one per line. 2: checkout URL. 549 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/LocalHandler.php:173550 1166 #: src/PHP/Commerce/LocalHandler.php:173 551 1167 #, php-format … … 558 1174 559 1175 #. 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 562 1177 #, php-format 563 1178 msgid "Order %1$s is currently: %2$s (placed on %3$s)." 564 1179 msgstr "" 565 1180 566 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:197567 1181 #: src/PHP/Commerce/Woo/LeadService.php:197 568 1182 msgid "Human support needed" 569 1183 msgstr "" 570 1184 571 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:198572 1185 #: src/PHP/Commerce/Woo/LeadService.php:198 573 1186 msgid "Priority:" 574 1187 msgstr "" 575 1188 576 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:199577 1189 #: src/PHP/Commerce/Woo/LeadService.php:199 578 1190 msgid "Issue:" 579 1191 msgstr "" 580 1192 581 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:200582 1193 #: src/PHP/Commerce/Woo/LeadService.php:200 583 1194 msgid "Summary:" … … 585 1196 586 1197 #. translators: %d: conversation ID. 587 #: build/ninhle-ai-sales-assistant/src/PHP/Commerce/Woo/LeadService.php:202588 1198 #: src/PHP/Commerce/Woo/LeadService.php:202 589 1199 #, php-format … … 591 1201 msgstr "" 592 1202 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 1204 msgid "Ninhle AI Sales Assistant: background replies are blocked by your host" 1205 msgstr "" 1206 1207 #: src/PHP/Core/AsyncKick.php:343 1208 msgid "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." 1209 msgstr "" 1210 1211 #: src/PHP/Core/AsyncKick.php:344 1212 msgid "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." 1213 msgstr "" 1214 601 1215 #: src/PHP/Privacy/PersonalData.php:35 602 1216 #: src/PHP/Privacy/PersonalData.php:43 … … 604 1218 msgstr "" 605 1219 606 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:144607 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:206608 1220 #: src/PHP/Privacy/PersonalData.php:144 609 #: src/PHP/Privacy/PersonalData.php:2 061221 #: src/PHP/Privacy/PersonalData.php:229 610 1222 msgid "Conversation" 611 1223 msgstr "" 612 1224 613 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:145614 1225 #: src/PHP/Privacy/PersonalData.php:145 615 1226 msgid "Sender" 616 1227 msgstr "" 617 1228 618 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:146619 1229 #: src/PHP/Privacy/PersonalData.php:146 620 1230 msgid "Message" 621 1231 msgstr "" 622 1232 623 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:147624 1233 #: src/PHP/Privacy/PersonalData.php:147 625 1234 msgid "Sent" 626 1235 msgstr "" 627 1236 628 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:153629 1237 #: src/PHP/Privacy/PersonalData.php:153 630 1238 msgid "Sender first name" 631 1239 msgstr "" 632 1240 633 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:154634 1241 #: src/PHP/Privacy/PersonalData.php:154 635 1242 msgid "Sender last name" 636 1243 msgstr "" 637 1244 638 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:155639 1245 #: src/PHP/Privacy/PersonalData.php:155 640 1246 msgid "Sender avatar" 641 1247 msgstr "" 642 1248 643 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PersonalData.php:164644 1249 #: src/PHP/Privacy/PersonalData.php:164 645 1250 msgid "Ninhle AI Sales Assistant chat messages" 646 1251 msgstr "" 647 1252 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 1254 msgid "Order linked to this chat" 1255 msgstr "" 1256 1257 #: src/PHP/Privacy/PersonalData.php:208 1258 msgid "Question the assistant could not answer (contact details hidden)" 1259 msgstr "" 1260 1261 #: src/PHP/Privacy/PersonalData.php:211 1262 msgid "Customer identified in this chat" 1263 msgstr "" 1264 1265 #. translators: %d: WordPress user ID of the logged-in customer. 1266 #: src/PHP/Privacy/PersonalData.php:213 1267 #, php-format 1268 msgid "Logged-in account #%d" 1269 msgstr "" 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 1274 msgid "Order ID %d (ownership checked)" 1275 msgstr "" 1276 1277 #: src/PHP/Privacy/PersonalData.php:218 1278 msgid "Reason the chat did not lead to an order (AI label)" 1279 msgstr "" 1280 1281 #: src/PHP/Privacy/PersonalData.php:222 1282 msgid "Answer to \"Did this answer your question?\"" 1283 msgstr "" 1284 1285 #: src/PHP/Privacy/PersonalData.php:226 650 1286 msgid "Ninhle AI Sales Assistant chat statistics" 651 1287 msgstr "" 652 1288 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 665 1290 msgid "Recorded (UTC)" 666 1291 msgstr "" 667 1292 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 682 1294 #: src/PHP/REST/LeadController.php:146 683 #: src/PHP/Widget/WidgetLoader.php:9 01295 #: src/PHP/Widget/WidgetLoader.php:95 684 1296 msgid "Email" 685 1297 msgstr "" 686 1298 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 691 1300 #: src/PHP/REST/LeadController.php:147 692 #: src/PHP/Widget/WidgetLoader.php:9 11301 #: src/PHP/Widget/WidgetLoader.php:96 693 1302 msgid "Phone" 694 1303 msgstr "" 695 1304 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 707 1306 msgid "Lead score" 708 1307 msgstr "" 709 1308 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 721 1310 msgid "Additional fields" 722 1311 msgstr "" 723 1312 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 726 1314 msgid "Ninhle AI Sales Assistant contact details" 727 1315 msgstr "" 728 1316 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 731 1318 msgid "Ninhle AI Sales Assistant chat sessions" 732 1319 msgstr "" 733 1320 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 743 1322 msgid "Session reference" 744 1323 msgstr "" 745 1324 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 748 1326 msgid "First seen" 749 1327 msgstr "" 750 1328 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 753 1330 msgid "Ninhle AI Sales Assistant activity log" 754 1331 msgstr "" 755 1332 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 758 1334 msgid "Action" 759 1335 msgstr "" 760 1336 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 763 1338 msgid "IP address" 764 1339 msgstr "" 765 1340 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 768 1342 msgid "Browser" 769 1343 msgstr "" 770 1344 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 779 1346 msgid "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." 780 1347 msgstr "" 781 1348 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 784 1350 msgid "This person was linked to records in a connected CRM. Copies already synced there must be erased in that CRM." 785 1351 msgstr "" 786 1352 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 789 1354 msgid "Customer records in Ninhle AI Sales Assistant CRM could not be erased because of a database error. Please run the erasure again." 790 1355 msgstr "" 791 1356 792 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:42793 1357 #: src/PHP/Privacy/PolicyContent.php:42 794 1358 msgid "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." 795 1359 msgstr "" 796 1360 797 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:46798 1361 #: src/PHP/Privacy/PolicyContent.php:46 799 1362 msgid "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." 800 1363 msgstr "" 801 1364 802 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:49803 1365 #: src/PHP/Privacy/PolicyContent.php:49 804 1366 msgid "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." 805 1367 msgstr "" 806 1368 807 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:53808 1369 #: src/PHP/Privacy/PolicyContent.php:53 809 1370 msgid "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." 810 1371 msgstr "" 811 1372 812 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:55813 1373 #: src/PHP/Privacy/PolicyContent.php:55 814 1374 msgid "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." … … 816 1376 817 1377 #. translators: %s is the name of the configured AI provider. 818 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:59819 1378 #: src/PHP/Privacy/PolicyContent.php:59 820 1379 #, php-format … … 822 1381 msgstr "" 823 1382 824 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:64825 1383 #: src/PHP/Privacy/PolicyContent.php:64 826 1384 msgid "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." 827 1385 msgstr "" 828 1386 829 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:68830 1387 #: src/PHP/Privacy/PolicyContent.php:68 831 1388 msgid "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." 832 1389 msgstr "" 833 1390 834 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:73835 1391 #: src/PHP/Privacy/PolicyContent.php:73 836 1392 msgid "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." 837 1393 msgstr "" 838 1394 839 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:77840 1395 #: src/PHP/Privacy/PolicyContent.php:77 841 1396 msgid "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." … … 843 1398 844 1399 #. translators: %d is the number of days files are kept. 845 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:84846 1400 #: src/PHP/Privacy/PolicyContent.php:84 847 1401 #, php-format … … 851 1405 msgstr[1] "" 852 1406 853 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:94854 1407 #: src/PHP/Privacy/PolicyContent.php:94 855 1408 msgid "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." 856 1409 msgstr "" 857 1410 858 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:99859 1411 #: src/PHP/Privacy/PolicyContent.php:99 860 1412 msgid "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." 861 1413 msgstr "" 862 1414 863 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:109864 1415 #: src/PHP/Privacy/PolicyContent.php:109 865 1416 msgid "OpenAI (see openai.com/policies/privacy-policy)" 866 1417 msgstr "" 867 1418 868 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:110869 1419 #: src/PHP/Privacy/PolicyContent.php:110 870 1420 msgid "Anthropic (see anthropic.com/legal/privacy)" 871 1421 msgstr "" 872 1422 873 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:111874 1423 #: src/PHP/Privacy/PolicyContent.php:111 875 1424 msgid "Google (see policies.google.com/privacy)" 876 1425 msgstr "" 877 1426 878 #: build/ninhle-ai-sales-assistant/src/PHP/Privacy/PolicyContent.php:114879 1427 #: src/PHP/Privacy/PolicyContent.php:114 880 1428 msgid "the AI provider configured by the store owner" 881 1429 msgstr "" 882 1430 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 885 1432 msgid "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)." 886 1433 msgstr "" 887 1434 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 890 1436 msgid "Sales Assistant" 891 1437 msgstr "" 892 1438 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 895 1440 msgid "Thanks, our team has been notified. A member of staff will reply here as soon as possible." 896 1441 msgstr "" 897 1442 898 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:98899 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:142900 1443 #: src/PHP/REST/Guard.php:98 901 1444 #: src/PHP/REST/Guard.php:142 … … 903 1446 msgstr "" 904 1447 905 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:124906 1448 #: src/PHP/REST/Guard.php:124 907 1449 msgid "Security check failed. Please reload the page and try again." 908 1450 msgstr "" 909 1451 910 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:165911 1452 #: src/PHP/REST/Guard.php:165 912 1453 msgid "Missing chat session." 913 1454 msgstr "" 914 1455 915 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:175916 1456 #: src/PHP/REST/Guard.php:175 917 1457 msgid "Invalid or expired chat session." 918 1458 msgstr "" 919 1459 920 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:184921 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:199922 1460 #: src/PHP/REST/Guard.php:184 923 1461 #: src/PHP/REST/Guard.php:199 … … 925 1463 msgstr "" 926 1464 927 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:215928 1465 #: src/PHP/REST/Guard.php:215 929 1466 msgid "Too many requests. Please wait a moment." 930 1467 msgstr "" 931 1468 932 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:242933 1469 #: src/PHP/REST/Guard.php:242 934 1470 msgid "Telegram webhook secret is not configured." 935 1471 msgstr "" 936 1472 937 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:255938 1473 #: src/PHP/REST/Guard.php:255 939 1474 msgid "Invalid webhook signature." 940 1475 msgstr "" 941 1476 942 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:308943 1477 #: src/PHP/REST/Guard.php:308 944 1478 msgid "Message must be text." 945 1479 msgstr "" 946 1480 947 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:313948 1481 #: src/PHP/REST/Guard.php:313 949 1482 msgid "Message contains invalid characters." 950 1483 msgstr "" 951 1484 952 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:317953 1485 #: src/PHP/REST/Guard.php:317 954 1486 msgid "Message cannot be empty." 955 1487 msgstr "" 956 1488 957 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:321958 1489 #: src/PHP/REST/Guard.php:321 959 1490 msgid "Message is too large." 960 1491 msgstr "" 961 1492 962 #: build/ninhle-ai-sales-assistant/src/PHP/REST/Guard.php:325963 1493 #: src/PHP/REST/Guard.php:325 964 1494 msgid "Message is too long." 965 1495 msgstr "" 966 1496 967 #: build/ninhle-ai-sales-assistant/src/PHP/REST/LeadController.php:149 1497 #: src/PHP/REST/InsightsController.php:43 1498 msgid "Enter both the question and the answer." 1499 msgstr "" 1500 1501 #: src/PHP/REST/InsightsController.php:66 1502 msgid "AI insights need the Starter plan." 1503 msgstr "" 1504 968 1505 #: src/PHP/REST/LeadController.php:149 969 1506 msgid "Score" 970 1507 msgstr "" 971 1508 972 #: build/ninhle-ai-sales-assistant/src/PHP/Rules/RuleEngine.php:232973 1509 #: src/PHP/Rules/RuleEngine.php:232 974 1510 msgid "A member of our team will join this chat shortly." 975 1511 msgstr "" 976 1512 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 979 1514 msgid "Open chat" 980 1515 msgstr "" 981 1516 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 1520 msgid "new replies: %d" 1521 msgstr "" 1522 1523 #: src/PHP/Widget/WidgetLoader.php:74 984 1524 msgid "Chat with our assistant" 985 1525 msgstr "" 986 1526 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 989 1528 msgid "Cart" 990 1529 msgstr "" 991 1530 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 994 1532 msgid "My details" 995 1533 msgstr "" 996 1534 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 999 1536 msgid "Clear conversation" 1000 1537 msgstr "" 1001 1538 1002 #: build/ninhle-ai-sales-assistant/src/PHP/Widget/WidgetLoader.php:731003 #: src/PHP/Widget/WidgetLoader.php: 731539 #: src/PHP/Widget/WidgetLoader.php:78 1540 #: src/PHP/Widget/WidgetLoader.php:139 1004 1541 msgid "Close" 1005 1542 msgstr "" 1006 1543 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 1009 1545 msgid "Clear the conversation history and start over?" 1010 1546 msgstr "" 1011 1547 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 1014 1549 msgid "Clear" 1015 1550 msgstr "" 1016 1551 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 1024 1553 msgid "Attach a photo or PDF" 1025 1554 msgstr "" 1026 1555 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 1029 1557 msgid "Uploading…" 1030 1558 msgstr "" 1031 1559 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 1034 1561 msgid "File upload failed." 1035 1562 msgstr "" 1036 1563 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 1039 1565 msgid "Your message could not be sent. Please try again." 1040 1566 msgstr "" 1041 1567 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 1044 1569 msgid "Call us" 1045 1570 msgstr "" 1046 1571 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 1049 1573 msgid "Send" 1050 1574 msgstr "" 1051 1575 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 1054 1577 msgid "Your message" 1055 1578 msgstr "" 1056 1579 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 1059 1581 msgid "Type your question…" 1060 1582 msgstr "" 1061 1583 1062 1584 #. 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 1065 1586 #, php-format 1066 1587 msgid "File is too large. Please choose a file under %sMB." 1067 1588 msgstr "" 1068 1589 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 1071 1591 msgid "Thank you. Our team will contact you shortly." 1072 1592 msgstr "" 1073 1593 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 1076 1595 msgid "Your contact details" 1077 1596 msgstr "" 1078 1597 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 1081 1599 msgid "Submit" 1082 1600 msgstr "" 1083 1601 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 1086 1603 msgid "Submitting…" 1087 1604 msgstr "" 1088 1605 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 1091 1607 msgid "Please provide at least an email or phone number." 1092 1608 msgstr "" 1093 1609 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 1096 1611 msgid "Could not submit your details." 1097 1612 msgstr "" 1098 1613 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 1101 1615 msgid "More options" 1102 1616 msgstr "" 1103 1617 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 1106 1619 msgid "Expand" 1107 1620 msgstr "" 1108 1621 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 1111 1623 msgid "Make smaller" 1112 1624 msgstr "" 1113 1625 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 1116 1627 msgid "Talk to a person" 1117 1628 msgstr "" 1118 1629 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 1121 1631 msgid "AI assistant" 1122 1632 msgstr "" 1123 1633 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 1126 1635 msgid "Waiting for a team member" 1127 1636 msgstr "" 1128 1637 1129 1638 #. 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 1132 1640 #, php-format 1133 1641 msgid "%s is helping you" 1134 1642 msgstr "" 1135 1643 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 1138 1645 msgid "A team member is helping you" 1139 1646 msgstr "" 1140 1647 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 1143 1649 msgid "This conversation is closed" 1144 1650 msgstr "" 1145 1651 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 1148 1653 msgid "Writing a reply…" 1149 1654 msgstr "" 1150 1655 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 1153 1657 msgid "Checking product information…" 1154 1658 msgstr "" 1155 1659 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 1158 1661 msgid "Looking up your order…" 1159 1662 msgstr "" 1160 1663 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 1163 1665 msgid "Connecting you with a team member…" 1164 1666 msgstr "" 1165 1667 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 1669 msgid "Waiting in line…" 1670 msgstr "" 1671 1672 #: src/PHP/Widget/WidgetLoader.php:119 1673 msgid "Previous products" 1674 msgstr "" 1675 1676 #: src/PHP/Widget/WidgetLoader.php:120 1677 msgid "Next products" 1678 msgstr "" 1679 1680 #: src/PHP/Widget/WidgetLoader.php:121 1681 msgid "View" 1682 msgstr "" 1683 1684 #: src/PHP/Widget/WidgetLoader.php:122 1685 msgid "In stock" 1686 msgstr "" 1687 1688 #: src/PHP/Widget/WidgetLoader.php:124 1689 msgid "On backorder" 1690 msgstr "" 1691 1692 #. translators: %s: date the price was shown, e.g. "24 Oct 2026". 1693 #: src/PHP/Widget/WidgetLoader.php:126 1694 #, php-format 1695 msgid "Price on %s" 1696 msgstr "" 1697 1698 #: src/PHP/Widget/WidgetLoader.php:128 1699 msgid "Add to cart" 1700 msgstr "" 1701 1702 #: src/PHP/Widget/WidgetLoader.php:129 1703 msgid "Adding…" 1704 msgstr "" 1705 1706 #: src/PHP/Widget/WidgetLoader.php:130 1707 msgid "Choose options" 1708 msgstr "" 1709 1710 #: src/PHP/Widget/WidgetLoader.php:131 1711 msgid "Added to cart" 1712 msgstr "" 1713 1714 #: src/PHP/Widget/WidgetLoader.php:132 1715 msgid "View cart" 1716 msgstr "" 1717 1718 #: src/PHP/Widget/WidgetLoader.php:133 1719 msgid "Checkout" 1720 msgstr "" 1721 1722 #: src/PHP/Widget/WidgetLoader.php:134 1723 msgid "Sorry, this item just sold out." 1724 msgstr "" 1725 1726 #: src/PHP/Widget/WidgetLoader.php:135 1727 msgid "You can't add more of this item." 1728 msgstr "" 1729 1730 #: src/PHP/Widget/WidgetLoader.php:136 1731 msgid "Couldn't add to cart." 1732 msgstr "" 1733 1734 #: src/PHP/Widget/WidgetLoader.php:137 1735 msgid "Open the product page" 1736 msgstr "" 1737 1738 #: src/PHP/Widget/WidgetLoader.php:138 1739 msgid "Your cart" 1740 msgstr "" 1741 1742 #: src/PHP/Widget/WidgetLoader.php:140 1743 msgid "Loading your cart…" 1744 msgstr "" 1745 1746 #: src/PHP/Widget/WidgetLoader.php:141 1747 msgid "Couldn't load your cart." 1748 msgstr "" 1749 1750 #: src/PHP/Widget/WidgetLoader.php:143 1751 msgid "Subtotal" 1752 msgstr "" 1753 1754 #: src/PHP/Widget/WidgetLoader.php:144 1755 msgid "Discount" 1756 msgstr "" 1757 1758 #: src/PHP/Widget/WidgetLoader.php:146 1759 msgid "Your orders" 1760 msgstr "" 1761 1762 #. translators: %s: order number. 1763 #: src/PHP/Widget/WidgetLoader.php:148 1764 #, php-format 1765 msgid "Order #%s" 1766 msgstr "" 1767 1768 #. translators: %d: number of other items in the order. 1769 #: src/PHP/Widget/WidgetLoader.php:150 1770 #, php-format 1771 msgid "and %d more" 1772 msgstr "" 1773 1774 #: src/PHP/Widget/WidgetLoader.php:151 1775 msgid "Tracking number" 1776 msgstr "" 1777 1778 #: src/PHP/Widget/WidgetLoader.php:152 1779 msgid "Track" 1780 msgstr "" 1781 1782 #: src/PHP/Widget/WidgetLoader.php:153 1783 msgid "View order" 1784 msgstr "" 1785 1786 #: src/PHP/Widget/WidgetLoader.php:154 1168 1787 msgid "Did this answer your question?" 1169 1788 msgstr "" 1170 1789 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 1179 1791 msgid "Not yet" 1180 1792 msgstr "" 1181 1793 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 1184 1795 msgid "Skip" 1185 1796 msgstr "" 1186 1797 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 1189 1799 msgid "Thanks for letting us know." 1190 1800 msgstr "" 1191 1801 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 1194 1803 msgid "Other ways to reach us" 1195 1804 msgstr "" 1196 1805 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 1199 1807 msgid "Speak your message" 1200 1808 msgstr "" 1201 1809 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 1204 1811 msgid "Stop listening" 1205 1812 msgstr "" 1206 1813 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 1209 1815 msgid "Listening…" 1210 1816 msgstr "" 1211 1817 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 1214 1819 msgid "Microphone access is blocked. Allow it in your browser settings, or type your message." 1215 1820 msgstr "" 1216 1821 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 1219 1823 msgid "No speech was heard. Please try again." 1220 1824 msgstr "" 1221 1825 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 1224 1827 msgid "No microphone was found. Please type your message." 1225 1828 msgstr "" 1226 1829 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 1229 1831 msgid "Voice input needs an internet connection. Please type your message." 1230 1832 msgstr "" 1231 1833 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 1234 1835 msgid "Your browser cannot recognise this language. Please type your message." 1235 1836 msgstr "" 1236 1837 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 1239 1839 msgid "Voice input is not available right now. Please type your message." 1240 1840 msgstr "" 1241 1841 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 1244 1843 msgid "You are chatting with an AI assistant. A team member can join when needed." 1245 1844 msgstr "" 1246 1247 #: premium/CRM/CrmStore.php:6091248 msgid "Customer ID"1249 msgstr ""1250 1251 #: premium/CRM/CrmStore.php:6111252 msgid "Verified"1253 msgstr ""1254 1255 #: premium/CRM/CrmStore.php:6111256 msgid "No"1257 msgstr ""1258 1259 #. translators: %s: contact type, for example "email" or "phone".1260 #: premium/CRM/CrmStore.php:6161261 #, php-format1262 msgid "Contact (%s)"1263 msgstr ""1264 1265 #: premium/CRM/CrmStore.php:6201266 msgid "Ninhle AI Sales Assistant customer profile"1267 msgstr ""1268 1269 #: premium/CRM/CrmStore.php:6231270 msgid "Ninhle AI Sales Assistant contact permissions"1271 msgstr ""1272 1273 #: premium/CRM/CrmStore.php:6241274 msgid "Purpose"1275 msgstr ""1276 1277 #: premium/CRM/CrmStore.php:6271278 msgid "Source"1279 msgstr ""1280 1281 #: premium/CRM/CrmStore.php:6281282 msgid "Recorded"1283 msgstr ""1284 1285 #: premium/CRM/CrmStore.php:6341286 msgid "Ninhle AI Sales Assistant purchase opportunities"1287 msgstr ""1288 1289 #: premium/CRM/CrmStore.php:6351290 msgid "Need"1291 msgstr ""1292 1293 #: premium/CRM/CrmStore.php:6361294 msgid "Stage"1295 msgstr ""1296 1297 #: premium/CRM/CrmStore.php:6371298 msgid "Payment"1299 msgstr ""1300 1301 #: premium/CRM/CrmStore.php:6381302 msgid "Products"1303 msgstr ""1304 1305 #: premium/CRM/CrmStore.php:6641306 msgid "Ninhle AI Sales Assistant customer history"1307 msgstr ""1308 1309 #: premium/CRM/CrmStore.php:6651310 #: premium/CRM/CrmStore.php:6751311 msgid "Type"1312 msgstr ""1313 1314 #: premium/CRM/CrmStore.php:6671315 msgid "Content"1316 msgstr ""1317 1318 #: premium/CRM/CrmStore.php:6741319 msgid "Ninhle AI Sales Assistant support and follow-up requests"1320 msgstr ""1321 1322 #: premium/CRM/CrmStore.php:6771323 msgid "Resolution"1324 msgstr ""1325 1326 #: premium/Integrations/TelegramService.php:771327 msgid "None"1328 msgstr ""1329 1330 #. translators: %s: chat widget brand name.1331 #: premium/Integrations/TelegramService.php:791332 #, php-format1333 msgid "New lead from %s"1334 msgstr ""1335 1336 #: premium/Integrations/TelegramService.php:801337 msgid "Name:"1338 msgstr ""1339 1340 #: premium/Integrations/TelegramService.php:811341 msgid "Email:"1342 msgstr ""1343 1344 #: premium/Integrations/TelegramService.php:821345 msgid "Phone:"1346 msgstr ""1347 1348 #: premium/Webhooks/WebhookDispatcher.php:1131349 msgid "Outgoing webhooks are available in the Pro plan."1350 msgstr "" -
ninhle-ai-sales-assistant/trunk/ninhle-ai-sales-assistant.php
r3761175 r3765552 1 1 <?php 2 2 /** 3 * Plugin Name: Ninhle AI Sales Assistant for WooCommerce4 * Description: AI sales and customer support for WooCommerce, powered by store data with product guidance, lead capture, and human handoff.5 * Version: 2. 5.03 * 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 6 6 * Author: Ninhle 7 7 * Author URI: https://ninhle.com … … 37 37 38 38 // Define Plugin Constants 39 define('SMBCOMMERCE_VERSION', '2. 5.0');39 define('SMBCOMMERCE_VERSION', '2.6.0'); 40 40 define('SMBCOMMERCE_FILE', __FILE__); 41 41 define('SMBCOMMERCE_PATH', plugin_dir_path(__FILE__)); -
ninhle-ai-sales-assistant/trunk/package-lock.json
r3761175 r3765552 1 1 { 2 2 "name": "ninhle-ai-sales-assistant-frontend", 3 "version": "2. 5.0",3 "version": "2.6.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "ninhle-ai-sales-assistant-frontend", 9 "version": "2. 5.0",9 "version": "2.6.0", 10 10 "dependencies": { 11 11 "preact": "^10.23.1", -
ninhle-ai-sales-assistant/trunk/package.json
r3761175 r3765552 2 2 "name": "ninhle-ai-sales-assistant-frontend", 3 3 "private": true, 4 "version": "2. 5.0",4 "version": "2.6.0", 5 5 "type": "module", 6 6 "scripts": { -
ninhle-ai-sales-assistant/trunk/readme.txt
r3762743 r3765552 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 2. 5.07 Stable tag: 2.6.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 AI chatbot and shopping assistant for WooCommerce: answers from your real catalog, tracks orders, hands off to your team, shows AI-assisted orders.11 AI chatbot and shopping assistant for WooCommerce: answers from your catalog, adds to cart in chat, tracks orders and shows why shoppers don't buy. 12 12 13 13 == Description == 14 14 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. 16 16 17 17 Shoppers 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. … … 19 19 = Get started in three steps = 20 20 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.21 1. **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. 22 22 2. **Sync your catalog.** One click, or `wp ninhle-ai-sales-assistant sync-products`, and the assistant knows your products, prices and stock. 23 23 3. **Go live.** The chat appears on your store with your name, logo, colour and suggested questions. 24 24 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 27 Product 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 29 In 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 33 Product 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. 32 34 33 35 = Order tracking in the chat = 34 36 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 41 The 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 45 The 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 47 In 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. 36 48 37 49 = A person is one tap away = 38 50 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. 40 52 41 53 = See the orders the chatbot helped close = … … 43 55 The 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. 44 56 57 = Ready for AI agents (MCP) = 58 59 On 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 45 61 = Your key, your data, no per-conversation fee = 46 62 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.63 Use 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. 48 64 49 65 = Everything in the free WooCommerce chatbot = 50 66 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. 66 83 67 84 = Optional paid plans = … … 70 87 plugin; nothing in the free version is locked or time-limited. 71 88 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; 90 nightly AI labels of why shoppers did not buy, with a weekly email summary; a customer card in 91 the Inbox; product variations with per-variation add-to-cart links; Telegram notifications for 92 new leads and handoffs; a sales dashboard; cached answers for repeated questions; and the 93 revenue of orders that followed a chat, per currency, on the AI Value & Performance tab. 76 94 77 95 **Pro** — everything in Starter, plus coupon validation, automatic failover to a … … 91 109 conversation history, your sales playbook and business information, and product data from 92 110 your own store, each time a visitor sends a message. Nothing is sent in local-only mode. 111 If you choose "WordPress AI connection" (WordPress 7.0+), the same data goes through WordPress 112 to the provider you connected in Settings > Connectors, under that provider's terms. 93 113 94 114 * Anthropic (Claude) — https://www.anthropic.com/legal/privacy — https://www.anthropic.com/legal/consumer-terms 95 115 * OpenAI — https://openai.com/policies/privacy-policy — https://openai.com/policies/terms-of-use 96 116 * 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 119 version contains no code for this. If you turn it on, once a night the text of finished chats 120 that did not lead to an order is sent to your AI provider, with email addresses and phone 121 numbers replaced by placeholders, so the AI can label why the shopper did not buy. A monthly 122 spending limit you set stops the job. 97 123 98 124 **Link reading (optional, off by default)** — when you enable "Let the bot read links … … 154 180 permission_callback, and the endpoints the AI uses as tools are internal-only - 155 181 they cannot be reached from outside the site. Visitor sessions use hashed tokens 156 and chat messages are rate limited per session. 182 and chat messages are rate limited per session. When a background AI reply is started 183 through a loopback request, that request is signed and expires after 60 seconds. 184 185 The plugin server never changes a shopper's cart: "Add to cart" in the chat calls 186 the WooCommerce Store API from the shopper's own browser. The AI can only propose 187 products that come from your catalog, and the card always reads the current price 188 and stock from WooCommerce. 157 189 158 190 Chat messages are rendered as DOM elements rather than injected HTML, and only … … 178 210 paragraph about this to Settings > Privacy. 179 211 212 Sales Insights stores the questions the assistant could not answer, with email addresses 213 and phone numbers hidden, and in the Starter plan the reason a chat did not lead to an 214 order with a short quote. When a logged-in customer chats, or a customer proves they own 215 an order, the conversation records that account or order number, so your team can see 216 the customer card (Starter plan). The language of each conversation is stored to reply in 217 it. All of this is included in the personal-data export and removed by the eraser. 218 219 In the visitor's browser, the chat keeps its session token and, for the new-replies badge, 220 the conversation number, the last message seen and when it was seen (localStorage). No 221 message text is stored there. 222 180 223 == Installation == 181 224 … … 192 235 AI provider for your own API key, or use local-only mode with no key at all. 193 236 237 = Can this WooCommerce chatbot add products to the cart? = 238 239 Yes. Product answers show cards with an Add to cart button, and the cart is updated by WooCommerce 240 in the shopper's browser, so your prices, stock and cart rules apply. In the free version, products 241 with options link to the product page; the Starter plan lets shoppers choose options and quantities 242 on the card. Nothing is added until the shopper taps the button. 243 194 244 = Can customers track their orders in the chat? = 195 245 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. 246 Yes. The assistant shows an order card with the status, items and tracking number. Logged-in 247 customers can ask for their recent orders. Guests give the order number together with the email 248 address 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 252 Yes, by default on new installs. It detects the language of each message and answers in it. Sites 253 upgraded from an earlier version keep replying in the store's language until you change "Reply 254 language" in Settings. 199 255 200 256 = Which AI models does the chatbot work with? = 201 257 202 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. You choose the 258 Anthropic Claude, OpenAI and Google Gemini, with your own API key for each. On WordPress 7.0 and 259 later you can also use the AI provider connected in Settings > Connectors. You choose the 203 260 provider and can switch at any time. 204 261 262 = What does Sales Insights show? = 263 264 The questions shoppers asked that the assistant could not answer, grouped and ranked, what they 265 searched for but could not find, and which products they wanted while out of stock. You can add 266 an answer from the same screen. The Starter plan adds the reasons chats did not lead to an order and a 267 weekly email summary. 268 269 = Can AI agents read my product catalog (MCP)? = 270 271 Yes, if you allow it. On WordPress 6.9 and later, turn on "AI agents" in Settings: the plugin 272 registers read-only abilities for product search, product details and store policies. With the 273 MCP Adapter plugin, MCP clients can use them. Orders, carts and customer data are never shared. 274 205 275 = Does the shopping assistant recommend products? = 206 276 207 277 Yes. 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.278 asked for and shows them as cards. It only recommends products that exist in your store. 209 279 210 280 = Do I need a paid AI subscription? = … … 213 283 and are billed directly by that provider. Local-only mode needs no key. 214 284 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 287 It depends on the provider and model you choose, and you pay the provider directly. In our test 288 of 60 conversations on a real store with version 2.6.0, GPT-4o mini cost about US$0.0009 per 289 conversation and Gemini 3.8 Flash about US$0.015. The AI Value & Performance tab shows what your 290 own store actually spent. 220 291 221 292 = Can I try it without an API key? = 222 293 223 Yes. Local-only mode answers product, cart and order questions from your store data without224 calling any AI provider. Add a key later when you want full conversations.294 Yes. Local-only mode answers greetings, store policies, cart and order questions from your store 295 data without calling any AI provider. Add a key later when you want full conversations. 225 296 226 297 = Will it slow down my store? = 227 298 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. 299 The chat button is small. The chat window, product cards, cart view and voice input load only 300 when they are needed, and AI replies run in the background after a message is sent. While the 301 chat is minimized after a recent conversation, it checks for new replies every 25 seconds, only 302 while the browser tab is visible. Nothing is loaded from third-party servers unless you pick a 303 Google font. 230 304 231 305 = Is my store data sent to you? = … … 237 311 = Does it work without WooCommerce? = 238 312 239 The chat and lead capture work on any WordPress site. Product search, catalog grounding and240 order lookup require WooCommerce.313 The chat and lead capture work on any WordPress site. Product search, product cards, catalog 314 grounding and order lookup require WooCommerce. 241 315 242 316 = Will it invent prices or stock? = 243 317 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. 318 No. Product facts come from lookups against your store, and product cards read the current price 319 and stock from WooCommerce when the reply is shown. If the catalog has not been synced, the bot 320 is told to say so and offer a handoff instead of guessing. 246 321 247 322 = What counts as an "AI-assisted order"? = … … 249 324 An order placed within your attribution window (24 hours, 72 hours or 7 days) after the 250 325 customer 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 left252 out, and partial refunds are deducted. AI-assisted does not mean the AI was the only reason 253 for the order.326 in the chat, or added a product to the cart from the chat. Each order is counted once. 327 Cancelled, failed and fully refunded orders are left out, and partial refunds are deducted. 328 AI-assisted does not mean the AI was the only reason for the order. 254 329 255 330 = Is anything in the free version locked? = … … 274 349 275 350 == 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. 276 375 277 376 = 2.5.0 = … … 370 469 == Upgrade Notice == 371 470 471 = 2.6.0 = 472 Add 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 372 474 = 2.5.0 = 373 475 Replies 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. … … 378 480 = 2.4.0 = 379 481 New 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 2 2 import { api } from '../api'; 3 3 import { ConfirmBox, NoticeList, useConfirm, useNotices } from './Feedback'; 4 import { CustomerCard, CustomerCardData } from './CustomerCard'; 4 5 5 6 // Nhãn hiển thị (2.5.0): chủ shop đọc "Waiting for your team" thay vì mã human_active. … … 24 25 }; 25 26 26 export function Inbox() { 27 /** openId: mở thẳng hội thoại này (link "Open chat" trong Sales Insights). */ 28 export function Inbox({ openId = null }: { openId?: number | null } = {}) { 27 29 const { notices, push, dismiss } = useNotices(); 28 30 const confirmation = useConfirm(); … … 37 39 const timeoutRef = useRef<number | null>(null); 38 40 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 }; 39 50 40 51 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]); 41 59 42 60 const loadConversations = async () => { … … 49 67 const selectConversation = async (conv: any) => { 50 68 setActiveConv(conv); 69 shownIdRef.current = conv.id; 70 setCustomer(null); 71 loadCustomer(conv.id); 51 72 try { 52 73 const data = await api.getMessages(conv.id); … … 68 89 scrollToBottom(); 69 90 pollIntervalRef.current = 5000; 91 loadCustomer(activeConv.id); 70 92 } 71 93 } catch { pollIntervalRef.current = Math.min(pollIntervalRef.current * 2, 60000); } … … 86 108 try { 87 109 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; } 89 111 setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; }); 90 112 loadConversations(); … … 111 133 try { 112 134 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; } 114 136 setSelectedIds(new Set()); 115 137 loadConversations(); … … 197 219 </div> 198 220 </div> 221 {customer && ( 222 <details className="smb-customer-inline"> 223 <summary>Customer: {customer.name || 'Guest customer'}</summary> 224 <CustomerCard card={customer} /> 225 </details> 226 )} 199 227 <div className="smb-commerce-chat-history"> 200 228 {messages.map(m => ( … … 221 249 )} 222 250 </div> 251 {activeConv && customer && ( 252 <aside className="smb-customer-aside" aria-label="Customer"> 253 <CustomerCard card={customer} /> 254 </aside> 255 )} 223 256 </div> 224 257 </> -
ninhle-ai-sales-assistant/trunk/src/JS/admin/components/Settings.tsx
r3761175 r3765552 38 38 smbcommerce_hide_on_checkout: '0', 39 39 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', 40 47 smbcommerce_gemini_model: '', 41 48 smbcommerce_anthropic_model: '', … … 74 81 const [features, setFeatures] = useState<any>({}); 75 82 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); 76 87 const [modelReport, setModelReport] = useState<any>(null); 77 88 // 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. … … 179 190 smbcommerce_hide_on_checkout: data.settings.smbcommerce_hide_on_checkout ?? '0', 180 191 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', 181 199 smbcommerce_gemini_model: data.settings.smbcommerce_gemini_model || '', 182 200 smbcommerce_anthropic_model: data.settings.smbcommerce_anthropic_model || '', … … 199 217 setFeatures(data.settings.smbcommerce_features || {}); 200 218 setPremiumBuild(data.settings.smbcommerce_premium_build !== false); 219 setWpAiAvailable(data.settings.smbcommerce_wp_ai_available === true); 220 setAbilitiesSupported(data.settings.smbcommerce_abilities_supported === true); 201 221 setModelReport(data.settings.smbcommerce_model_report || null); 202 222 setSampleBusinessInfo(data.settings.smbcommerce_business_info_is_sample ? data.settings.smbcommerce_business_info : null); … … 247 267 smbcommerce_hide_on_checkout: settings.smbcommerce_hide_on_checkout, 248 268 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, 249 276 smbcommerce_gemini_model: settings.smbcommerce_gemini_model, 250 277 smbcommerce_anthropic_model: settings.smbcommerce_anthropic_model, … … 562 589 563 590 <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"> 564 609 <label>AI provider</label> 565 610 <select … … 570 615 <option value="anthropic">Anthropic (Claude 3.5)</option> 571 616 <option value="gemini">Google Gemini</option> 617 {(wpAiAvailable || settings.smbcommerce_ai_provider === 'wordpress') && ( 618 <option value="wordpress">WordPress AI connection (Settings → Connectors)</option> 619 )} 572 620 </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 )} 573 628 </div> 574 629 … … 1041 1096 1042 1097 <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"> 1043 1194 <h3><span className="dashicons dashicons-admin-tools"></span> Advanced features</h3> 1044 1195 -
ninhle-ai-sales-assistant/trunk/src/JS/admin/index.tsx
r3752682 r3765552 1 import React, { use State } from 'react';1 import React, { useEffect, useState } from 'react'; 2 2 import { createRoot } from 'react-dom/client'; 3 3 import { Inbox } from './components/Inbox'; … … 6 6 import { Settings } from './components/Settings'; 7 7 import { ValueDashboard } from './components/ValueDashboard'; 8 import { loadInsights } from './insightsLoader'; 9 import { t } from './i18n'; 8 10 import './style.css'; 9 11 10 const TABS = ['value', 'in box', 'rules', 'leads'];12 const TABS = ['value', 'insights', 'inbox', 'rules', 'leads']; 11 13 const TAB_KEY = 'smbcommerce_admin_tab'; 12 14 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. */ 14 16 function initialTab() { 17 const fromUrl = new URLSearchParams(window.location.search).get('smb_tab'); 18 if (fromUrl && TABS.includes(fromUrl)) return fromUrl; 15 19 try { 16 20 const saved = window.localStorage.getItem(TAB_KEY); … … 23 27 const AdminApp = () => { 24 28 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); 25 32 const setActiveTab = (tab: string) => { 26 33 setActiveTabState(tab); … … 28 35 }; 29 36 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 ); 30 53 31 54 return ( … … 33 56 <h1>Ninhle AI Sales Assistant Admin</h1> 34 57 <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')} 39 63 </h2> 40 64 <div style={{ marginTop: 20 }}> 41 65 {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} />} 43 72 {activeTab === 'rules' && <Rules />} 44 73 {activeTab === 'leads' && <Leads />} -
ninhle-ai-sales-assistant/trunk/src/JS/admin/style.css
r3752682 r3765552 374 374 } 375 375 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 376 440 .smb-commerce-main-input { 377 441 padding: 15px; … … 656 720 outline-offset: 2px; 657 721 } 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 40 40 }, 41 41 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 */ 42 46 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`, { 44 54 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 51 57 }); 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 } 52 64 if (!res.ok) throw new Error('Failed to send message'); 53 65 return await res.json(); … … 69 81 * 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. 70 82 */ 71 event(conversationId: number, type: string, ref = '' ) {83 event(conversationId: number, type: string, ref = '', extra: Record<string, number> = {}) { 72 84 fetch(`${BASE_URL}/chat/event`, { 73 85 method: 'POST', 74 86 headers: this.getHeaders(), 75 body: JSON.stringify({ conversation_id: conversationId, type, ref }),87 body: JSON.stringify({ conversation_id: conversationId, type, ref, ...extra }), 76 88 keepalive: true 77 89 }).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 8 8 config: any; 9 9 buttonRef?: Ref<HTMLButtonElement>; 10 /** Số tin chưa đọc khi khung chat thu nhỏ (2.6.0, S9). */ 11 unread?: number; 10 12 } 11 13 12 export function ChatButton({ onClick, onIntent, config, buttonRef }: ChatButtonProps) {14 export function ChatButton({ onClick, onIntent, config, buttonRef, unread = 0 }: ChatButtonProps) { 13 15 return ( 14 16 <button … … 20 22 onTouchStart={onIntent} 21 23 onFocus={onIntent} 22 aria-label={ t('openChat')}24 aria-label={unread > 0 ? `${t('openChat')} (${t('unreadMessages').replace('%d', String(unread))})` : t('openChat')} 23 25 aria-haspopup="dialog" 24 26 > … … 30 32 </svg> 31 33 )} 34 {unread > 0 && <span className="smb-unread" aria-hidden="true">{unread > 9 ? '9+' : unread}</span>} 32 35 </button> 33 36 ); -
ninhle-ai-sales-assistant/trunk/src/JS/widget/components/ChatWindow.tsx
r3761175 r3765552 6 6 import { toPlainText } from '../markdown'; 7 7 import { t } from '../i18n'; 8 import { nextDelay, nextError, nextIdle, POLL } from '../pollSchedule'; 8 9 import type { VoiceSession } from '../voice'; 9 10 10 11 type VoiceModule = typeof import('../voice'); 12 type CardsModule = typeof import('./ProductCards'); 13 type 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 */ 19 function 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 } 11 24 12 25 /** 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ó). */ … … 50 63 order: 'stageOrder', 51 64 handoff: 'stageHandoff', 65 queued: 'stageQueued', 52 66 }; 53 67 … … 108 122 const [listening, setListening] = useState(false); 109 123 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); 110 131 111 132 const voiceModRef = useRef<VoiceModule | null>(null); … … 118 139 const cancelClearRef = useRef<HTMLButtonElement>(null); 119 140 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'); 121 146 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); 122 151 123 152 const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; … … 128 157 129 158 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]); 130 165 131 166 // 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 … … 181 216 182 217 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; } 184 219 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; 186 222 }; 187 223 188 224 const poll = async () => { 189 225 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); 191 228 return; 192 229 } 230 let gotMessages = false; 193 231 try { 194 232 const data = await api.pollMessages(conversationId, lastMessageIdRef.current); … … 201 239 const incoming = data.messages.filter((m: any) => m.sender_type !== 'user'); 202 240 if (incoming.length > 0) { 241 awaitingSinceRef.current = 0; 203 242 setIsTyping(false); 204 243 setStage(null); … … 206 245 setLiveText(toPlainText(incoming[incoming.length - 1].content).slice(0, 300)); 207 246 } 208 pollIntervalRef.current = 5000;247 gotMessages = true; 209 248 } 249 errorDelayRef.current = 0; 250 idleDelayRef.current = nextIdle(idleDelayRef.current, gotMessages); 210 251 } 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 }); 215 265 216 266 useEffect(() => { … … 219 269 const handleVisibility = () => { 220 270 if (document.visibilityState === 'visible') { 221 pollIntervalRef.current = 5000; 271 idleDelayRef.current = POLL.idleStart; 272 errorDelayRef.current = 0; 222 273 if (timeoutRef.current) clearTimeout(timeoutRef.current); 223 274 poll(); … … 232 283 233 284 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. 235 286 if (timeoutRef.current) clearTimeout(timeoutRef.current); 236 287 timeoutRef.current = window.setTimeout(poll, 100); … … 244 295 }; 245 296 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. 247 298 const voiceLang = (mod: VoiceModule) => mod.pickLanguage( 248 document.documentElement.lang,299 languageRef.current || document.documentElement.lang, 249 300 navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language] 250 301 ); … … 300 351 const savedMsg = await api.sendMessage(conversationId, content, crypto.randomUUID()); 301 352 setMessages(prev => prev.map(m => m.id === tempMsg.id ? savedMsg : m)); 353 if (status === 'bot_active') awaitingSinceRef.current = Date.now(); 302 354 pollSoon(); 303 355 } catch (err) { … … 372 424 373 425 // 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 374 440 const trackLink = (url: string) => { 375 441 try { … … 445 511 </div> 446 512 <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 ) : ( 448 520 <a className="smb-icon-btn" href={config.cartUrl} aria-label={t('cart')} title={t('cart')}><Icon d={ICONS.cart} /></a> 449 ) }521 ))} 450 522 <button type="button" className="smb-icon-btn smb-expand-btn" onClick={() => setExpanded(!expanded)} 451 523 aria-pressed={expanded} aria-label={expanded ? t('collapse') : t('expand')} title={expanded ? t('collapse') : t('expand')}> … … 471 543 472 544 <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 )} 473 549 {config.aiNotice && <p className="smb-ai-notice">{config.aiNotice}</p>} 474 550 … … 484 560 )} 485 561 486 {messages.map(m => (562 {messages.map(m => [ 487 563 <div key={m.id} className={`smb-msg ${m.sender_type === 'user' ? 'is-user' : m.sender_type === 'agent' ? 'is-agent' : 'is-bot'}`}> 488 564 <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 ])} 491 574 492 575 {isTyping && ( -
ninhle-ai-sales-assistant/trunk/src/JS/widget/i18n.ts
r3761175 r3765552 11 11 const FALLBACK: Strings = { 12 12 openChat: 'Open chat', 13 unreadMessages: 'new replies: %d', 13 14 chatWindow: 'Chat with our assistant', 14 15 cart: 'Cart', … … 50 51 stageOrder: 'Looking up your order…', 51 52 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', 52 85 resolutionQuestion: 'Did this answer your question?', 53 86 yes: 'Yes', -
ninhle-ai-sales-assistant/trunk/src/JS/widget/index.tsx
r3761175 r3765552 4 4 import { api } from './api'; 5 5 import { onColor } from './color'; 6 import { PEEK_MS, countUnread, lastId, mergeMessages, readSeen, shouldPeek, writeSeen } from './peek'; 6 7 import './style.css'; 7 8 … … 41 42 const wasOpen = useRef(false); 42 43 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 }, []); 43 89 44 90 const prefetchChat = () => { … … 232 278 /> 233 279 ) : ( 234 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} />280 <ChatButton onClick={openChat} onIntent={prefetchChat} config={config} buttonRef={launcherRef} unread={unread} /> 235 281 )} 236 282 </div> -
ninhle-ai-sales-assistant/trunk/src/JS/widget/style.css
r3761175 r3765552 91 91 92 92 .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 } 94 113 .smb-launcher img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; } 95 114 … … 247 266 .is-bot .smb-bubble { background: var(--smb-bg-soft); color: var(--smb-text); border-bottom-left-radius: 6px; } 248 267 .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; } 249 364 250 365 .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 25 25 vi: 'vi-VN', en: 'en-US', th: 'th-TH', id: 'id-ID', ms: 'ms-MY', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN', 26 26 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', 28 28 }; 29 29 -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/AIProviderFactory.php
r3747521 r3765552 13 13 14 14 /** Khoá setting chứa API key tương ứng mỗi provider. */ 15 p rivatestatic function key_setting($provider) {15 public static function key_setting($provider) { 16 16 $map = [ 17 17 'openai' => 'smbcommerce_openai_key', … … 20 20 ]; 21 21 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)); 22 31 } 23 32 … … 37 46 if (class_exists('\\Smartbuy\SMBCommerce\\AI\\Providers\\GeminiProvider')) { 38 47 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(); 39 54 } 40 55 break; … … 65 80 $chain = []; 66 81 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)) { 69 83 continue; 70 84 } -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/AIService.php
r3752682 r3765552 87 87 $user_context .= "No user info provided yet. Do not assume their name.\n"; 88 88 } 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 } 90 95 91 96 // 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. … … 100 105 // WooCommerce Product RAG Context 101 106 $product_context = ''; 107 $context_ids = []; 102 108 if (class_exists('\\Smartbuy\SMBCommerce\\WooCommerce\\WooCommerceProductScanner')) { 103 109 $scanner = new \Smartbuy\SMBCommerce\WooCommerce\WooCommerceProductScanner(); … … 105 111 $latest_user_message !== '' ? [$latest_user_message] : [] 106 112 ); 113 $context_ids = $scanner->context_ids(); 107 114 } 108 115 … … 123 130 $sales_playbook = SalesPlaybook::build($playbook_state); 124 131 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()); 126 136 127 137 // Kịch bản đứng đầu để định hình vai trò, sau đó mới tới ràng buộc và dữ liệu. … … 130 140 $guardrails, 131 141 $capability_limits, 142 $cart_actions_prompt, 132 143 $master_prompt, 133 144 $handoff_instructions, 134 145 $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, 135 150 $user_context, 136 151 $history_digest, … … 152 167 $final_system_prompt .= "\n\n[SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES]\n" . $extracted_content; 153 168 } 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; 154 176 } 155 177 … … 187 209 188 210 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(); 189 213 $local_reply = \Smartbuy\SMBCommerce\Commerce\LocalHandler::handle($routing); 190 214 191 215 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); 193 219 self::log_ai_request($conversation_id, 'chat', 0, 0, 0, 'local', false, $routing['reason']); 194 220 return true; … … 237 263 \Smartbuy\SMBCommerce\Commerce\RoutingReason::UNRESOLVED_LOCAL_ONLY 238 264 ); 265 \Smartbuy\SMBCommerce\Insights\InsightsRecorder::unanswered($conversation_id, 'local_only'); 239 266 return true; 240 267 } … … 318 345 $rounds = 0; 319 346 $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; 320 349 // Gom kết quả tool để đối chiếu số liệu trong câu trả lời cuối. 321 350 $grounding_from_tools = ''; … … 329 358 'contact_id' => (int) ($conversation['contact_id'] ?? 0), 330 359 ]); 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); 331 364 332 365 while (!empty($tool_calls) && $rounds < self::MAX_TOOL_ROUNDS) { … … 341 374 self::set_stage($conversation_id, self::STAGE_BY_TOOL[$tool_name] ?? 'thinking'); 342 375 $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); 343 378 344 379 // Gọi tool handoff nghĩa là thật sự phải chuyển cho người thật, … … 346 381 if ($tool_name === 'create_handoff' && !empty($result['success'])) { 347 382 $tool_handoff_priority = $tool_params['priority'] ?? 'normal'; 383 $handoff_missing_info = ($tool_params['cause'] ?? '') === 'missing_information'; 348 384 } 349 385 … … 369 405 // Provider tự dựng message theo định dạng của mình. 370 406 $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 } 371 414 372 415 $call_start = microtime(true); … … 396 439 $reply_content = self::clean_reply($raw_reply); 397 440 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 398 452 // Hết vòng mà model vẫn đòi gọi tool: đừng trả về câu rỗng cho khách. 399 453 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'); 401 456 } 402 457 … … 450 505 } 451 506 } 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 } 452 519 453 520 // --- Đối chiếu mọi con số tiền với dữ liệu thật đã đưa cho model --- … … 474 541 $saved_reply = MessageRepository::add_message($conversation_id, 'system', $reply_content, null, $metadata); 475 542 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 } 476 551 477 552 // Chỉ cache khi lượt này không gọi tool nào — kết quả tool là dữ liệu sống. … … 535 610 'get_product_variations' => 'products', 536 611 'get_order_status' => 'order', 612 'get_my_orders' => 'order', 537 613 'get_customer_downloads' => 'order', 538 614 'create_handoff' => 'handoff', … … 679 755 * HỢP LỆ, bot trả lời "mã không hợp lệ". Trả về '' khi gói đã có đủ công cụ. 680 756 */ 681 private static function capability_limits( ) {757 private static function capability_limits($has_coupon_offers = false) { 682 758 $gate = '\\Smartbuy\\SMBCommerce\\Licensing\\LicenseGate'; 683 759 $limits = []; 684 760 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].' : ''); 686 763 } 687 764 if (!$gate::can('product_variations')) { … … 726 803 * và retry_count; rỗng với lượt không gọi AI (local, cache, local_only). 727 804 */ 728 p rivatestatic 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 = []) { 729 806 global $wpdb; 730 807 $table = $wpdb->prefix . SMBCOMMERCE_PREFIX . 'ai_logs'; -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/ClaudeProvider.php
r3752682 r3765552 172 172 } 173 173 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 174 215 public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) { 175 216 if (!empty($assistant_message)) { … … 199 240 ], 200 241 '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, 202 244 ]); 203 245 -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/GeminiProvider.php
r3752682 r3765552 19 19 // ngừng lần lượt và khi đó mọi lượt chat đều lỗi 404 (đo trên staging 2026-10-09). 20 20 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; 21 24 22 25 private $model = self::DEFAULT_MODEL; … … 116 119 117 120 $response = $this->call_api($payload); 118 121 119 122 if (is_wp_error($response)) { 120 123 return $response; 121 124 } 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 } 126 151 } 127 152 … … 136 161 $meta = $response['usageMetadata'] ?? []; 137 162 $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'], 141 166 'cache_write_tokens' => 0, 142 167 'model' => (string) ($response['modelVersion'] ?? $this->model), … … 149 174 'usage' => $usage, 150 175 '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 ], 151 233 ]; 152 234 } … … 188 270 } 189 271 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) { 191 276 $url = $this->build_url(); 192 277 … … 194 279 'headers' => $this->build_headers(), 195 280 '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, 197 283 ]); 198 284 … … 213 299 \Smartbuy\SMBCommerce\Support\Log::error('Ninhle AI Sales Assistant Gemini API Error (HTTP ' . $http_code . '): ' 214 300 . 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 } 215 310 return new \WP_Error('gemini_error', $err_msg . ' (HTTP ' . $http_code . ')'); 216 311 } -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/Providers/OpenAIProvider.php
r3752682 r3765552 182 182 } 183 183 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 184 223 public function append_tool_turn(&$messages_context, $assistant_message, $tool_results) { 185 224 if (!empty($assistant_message)) { … … 204 243 ], 205 244 '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, 207 247 ]); 208 248 -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/ResponseSanitizer.php
r3752682 r3765552 38 38 'IMPORTANT RULES', 39 39 "SUPPLEMENTAL CONTEXT EXTRACTED FROM USER'S LINKS/FILES", 40 // 2.6.0 41 'REPLY LANGUAGE', 42 'PRODUCT CARDS', 40 43 ]; 41 44 -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/SalesPlaybook.php
r3752682 r3765552 263 263 'hesitating' => self::contains($text, ['suy nghĩ', 'cân nhắc', 'để xem', 'từ từ', 'chưa cần', 'tính sau']), 264 264 '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']), 266 268 'asks_policy' => self::contains($text, ['bảo hành', 'đổi trả', 'ship', 'giao hàng', 'vận chuyển', 'hoàn tiền']), 267 269 ]; … … 275 277 } 276 278 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'])) { 278 283 $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.'; 279 284 } elseif (!empty($s['price_concern'])) { … … 311 316 } 312 317 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 313 324 private static function contains($haystack, $needles) { 314 325 foreach ($needles as $needle) { -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/ToolExecutor.php
r3747521 r3765552 38 38 'get_product_variations' => '/smb-commerce/v1/tools/product-variations', 39 39 'check_coupon' => '/smb-commerce/v1/tools/check-coupon', 40 'propose_cart_action' => '/smb-commerce/v1/tools/propose-cart-action', 40 41 'get_order_status' => '/smb-commerce/v1/tools/order-status', 42 'get_my_orders' => '/smb-commerce/v1/tools/my-orders', 41 43 'get_customer_downloads' => '/smb-commerce/v1/tools/customer-downloads', 42 44 'get_policy' => '/smb-commerce/v1/tools/policy', -
ninhle-ai-sales-assistant/trunk/src/PHP/AI/ToolsHelper.php
r3747521 r3765552 19 19 'get_product_variations' => 'product_variations', 20 20 'check_coupon' => 'coupon_tools', 21 'propose_cart_action' => 'cart_actions_advanced', 21 22 ]; 22 23 $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 } 24 30 if (!isset($paid_tools[$tool['name']])) { 25 31 return true; … … 110 116 ], 111 117 [ 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 [ 112 154 'name' => 'get_order_status', 113 155 '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.', … … 141 183 ], 142 184 [ 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 [ 143 195 'name' => 'get_customer_downloads', 144 196 '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.', … … 201 253 'enum' => ['low', 'normal', 'high', 'urgent'], 202 254 '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.' 203 261 ], 204 262 'email' => [ -
ninhle-ai-sales-assistant/trunk/src/PHP/Admin/AdminMenu.php
r3752682 r3765552 75 75 'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')), 76 76 '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(), 79 79 ]; 80 80 … … 84 84 $config 85 85 ); 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 ]; 86 173 } 87 174 -
ninhle-ai-sales-assistant/trunk/src/PHP/Analytics/OrderAttribution.php
r3752682 r3765552 24 24 const META_INTERACTION_AT = '_smbcommerce_interaction_at'; 25 25 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 26 29 public static function init() { 27 30 add_action('woocommerce_checkout_order_processed', [self::class, 'on_classic_checkout'], 20, 3); … … 32 35 33 36 /** 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ờ. 37 42 * @return bool true nếu đã lưu được vào phiên. 38 43 */ 39 public static function remember_interaction($conversation_id ) {44 public static function remember_interaction($conversation_id, $at = null) { 40 45 $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')) { 42 59 return false; 43 60 } … … 66 83 $wc->session->set(self::SESSION_KEY, [ 67 84 'conversation_id' => $conversation_id, 68 'at' => gmdate('Y-m-d H:i:s'),85 'at' => $at, 69 86 ]); 70 87 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); 71 108 } 72 109 … … 220 257 221 258 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(); 223 260 } 224 261 } -
ninhle-ai-sales-assistant/trunk/src/PHP/Analytics/ValueEvents.php
r3752682 r3765552 24 24 const ORDER_STATUS = 'order_status'; 25 25 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'; 26 36 27 37 /** 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 187 187 'email' => $params['email'] ?? '', 188 188 'phone' => $params['phone'] ?? '', 189 'user_id' => (int) ($params['user_id'] ?? 0), 189 190 ]); 190 191 … … 210 211 $order = $result->data(); 211 212 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 212 216 return sprintf( 213 217 /* translators: 1: order number. 2: order status. 3: date the order was placed. */ 214 218 __('Order %1$s is currently: %2$s (placed on %3$s).', 'ninhle-ai-sales-assistant'), 215 219 $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'] 218 225 ); 219 226 } -
ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/LocalResolver.php
r3747521 r3765552 75 75 76 76 /** 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 /** 77 89 * Quyết định định tuyến cho một tin nhắn. 78 90 * … … 124 136 } 125 137 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)) { 128 142 return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'checkout'); 129 143 } 130 144 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)) { 132 147 return self::decision(RoutingReason::LOCAL_CART_COMMAND, 'view_cart'); 133 148 } … … 178 193 private static function length($text) { 179 194 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; 180 206 } 181 207 … … 253 279 } 254 280 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 255 287 if (empty($proof)) { 256 288 return null; -
ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/CartService.php
r3747521 r3765552 18 18 * Vì sao điều đó nguy hiểm ở đây: 19 19 * 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. 34 35 * 35 36 * Nguyên tắc: chỉ động vào giỏ khi chắc chắn đang ở trong request của chính … … 252 253 253 254 /** 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. 259 257 */ 260 258 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(); 278 260 } 279 261 -
ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/OrderService.php
r3747521 r3765552 33 33 const PUBLIC_FIELDS = [ 34 34 'id', 'order_number', 'status', 'date', 'total', 'currency', 'items', 35 'payment_method', 'shipping_method', 35 'payment_method', 'shipping_method', 'tracking', 36 36 ]; 37 37 … … 134 134 135 135 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)]); 136 171 } 137 172 … … 278 313 'payment_method' => $order->get_payment_method_title(), 279 314 '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)), 280 319 ]; 281 320 } -
ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/ProductImage.php
r3761175 r3765552 21 21 const SIZES = ['large', 'medium_large', 'medium', 'full']; 22 22 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 23 26 /** 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. 25 29 * @return string URL dùng được, hoặc chuỗi rỗng khi không có ảnh nào còn trên đĩa. 26 30 */ 27 public static function url($attachment_id ) {31 public static function url($attachment_id, array $sizes = self::SIZES) { 28 32 $attachment_id = (int) $attachment_id; 29 33 if ($attachment_id <= 0 || !function_exists('wp_get_attachment_image_src')) { … … 34 38 $tried = []; 35 39 36 foreach ( self::SIZESas $size) {40 foreach ($sizes as $size) { 37 41 $src = wp_get_attachment_image_src($attachment_id, $size); 38 42 $url = is_array($src) && !empty($src[0]) ? (string) $src[0] : ''; -
ninhle-ai-sales-assistant/trunk/src/PHP/Commerce/Woo/ProductService.php
r3761175 r3765552 57 57 // Danh mục rỗng khác hẳn với "không tìm thấy". Nói rõ để model khuyên 58 58 // 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 } 61 83 } 62 84 63 85 // 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'))); 72 166 } 73 167 -
ninhle-ai-sales-assistant/trunk/src/PHP/Conversation/ConversationRepository.php
r3752682 r3765552 30 30 } 31 31 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 32 44 public static function create_conversation($contact_id) { 33 45 global $wpdb; -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/Deactivator.php
r3752682 r3765552 13 13 wp_clear_scheduled_hook('smbcommerce_run_jobs'); 14 14 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(); 15 17 16 18 flush_rewrite_rules(); -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/JobDispatcher.php
r3752682 r3765552 9 9 * Nối hàng đợi job với phần xử lý thật. 10 10 * 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. 15 18 */ 16 19 class JobDispatcher { … … 18 21 const CRON_HOOK = 'smbcommerce_run_jobs'; 19 22 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; 20 33 21 34 public static function init() { … … 52 65 } 53 66 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 } 54 84 try { 55 85 \Smartbuy\SMBCommerce\AI\AIService::process_user_message( … … 70 100 ); 71 101 } 102 } finally { 103 \Smartbuy\SMBCommerce\AI\TurnClock::clear(); 72 104 } 73 105 } … … 81 113 82 114 /** 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] 85 119 */ 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; 88 170 } 89 171 } -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/JobQueue.php
r3747521 r3765552 12 12 * (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. 13 13 * 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. 19 23 */ 20 24 class JobQueue { … … 25 29 const MAX_ATTEMPTS = 3; 26 30 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'; 29 42 30 43 /** Job bị coi là kẹt (worker chết giữa chừng) sau ngần này giây. */ 31 44 const STALE_LOCK_SECONDS = 180; 32 45 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 33 52 private static function table() { 34 53 global $wpdb; … … 36 55 } 37 56 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 */ 38 63 public static function enqueue($action, $payload = []) { 39 64 global $wpdb; 40 65 66 $payload = self::conversation_first((array) $payload); 41 67 $now = current_time('mysql'); 42 68 43 $ wpdb->insert(self::table(), [69 $inserted = $wpdb->insert(self::table(), [ 44 70 'action' => $action, 45 71 'payload' => wp_json_encode($payload), … … 50 76 ]); 51 77 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. 58 98 * 59 99 * @return array|null Job đã giành được, hoặc null nếu không có gì để làm. … … 61 101 public static function claim($filter_action = null) { 62 102 global $wpdb; 63 $table = self::table();64 103 65 104 self::release_stale_locks(); 66 105 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 } 73 109 74 110 // Lọc theo action là tuỳ chọn: chuỗi rỗng nghĩa là mọi action (câu SQL cố định). 75 111 $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 79 116 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(), 82 119 $now, 83 120 $action, … … 85 122 ), ARRAY_A); 86 123 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') 109 172 ), ARRAY_A); 110 173 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; 116 220 } 117 221 … … 151 255 'status' => 'pending', 152 256 'locked_by' => null, 153 'payload' => wp_json_encode( $payload),257 'payload' => wp_json_encode(self::conversation_first($payload)), 154 258 'next_attempt_at' => gmdate('Y-m-d H:i:s', strtotime(current_time('mysql')) + $delay), 155 259 'updated_at' => current_time('mysql'), … … 161 265 } 162 266 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 163 279 /** 164 280 * Rút và chạy tối đa $limit job. Trả về số job đã xử lý. … … 173 289 } 174 290 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); 182 292 $processed++; 183 293 } … … 186 296 } 187 297 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 188 329 private static function running_count() { 189 330 global $wpdb; 190 331 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; 191 360 } 192 361 -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/MigrationManager.php
r3752682 r3765552 14 14 * 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, 15 15 * bảng value_events. 16 * 4 = 2.6.0: conversations.language (ngôn ngữ khách đang viết, S2). 16 17 */ 17 const SCHEMA_REVISION = 3;18 const SCHEMA_REVISION = 4; 18 19 19 20 /** Kết quả lần chuyển dữ liệu gần nhất, để chẩn đoán và cho test đọc. */ … … 64 65 // Cài mới: model đề xuất (chỉ khi benchmark đã duyệt). Nâng cấp không bao giờ đổi model đã lưu. 65 66 \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); 66 70 67 71 // Ngày bắt đầu đo của bảng "AI Value & Performance" (UTC): ghi một lần, không bao giờ ghi đè, … … 151 155 'outcome_at' => 'datetime DEFAULT NULL', 152 156 ]); 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 ]); 153 162 } 154 163 … … 208 217 outcome varchar(30) DEFAULT NULL, 209 218 outcome_at datetime DEFAULT NULL, 219 language varchar(16) DEFAULT NULL, 210 220 created_at datetime DEFAULT NULL, 211 221 updated_at datetime DEFAULT NULL, -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/Plugin.php
r3752682 r3765552 45 45 } 46 46 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 47 52 // Domain-lock notice — chỉ hiển thị khi license.php tồn tại và domain không khớp. 48 53 // Bản phát triển (không có license.php) hoàn toàn im lặng. … … 78 83 \Smartbuy\SMBCommerce\Core\JobDispatcher::init(); 79 84 } 85 if (class_exists('\\Smartbuy\SMBCommerce\\Core\\AsyncKick')) { 86 \Smartbuy\SMBCommerce\Core\AsyncKick::init(); 87 } 80 88 81 89 if (class_exists('\\Smartbuy\SMBCommerce\\Core\\CronManager')) { … … 90 98 \Smartbuy\SMBCommerce\Analytics\OrderAttribution::init(); 91 99 } 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 } 92 110 } 93 111 } -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/PremiumFallbacks.php
r3747521 r3765552 30 30 'Smartbuy\SMBCommerce\\Commerce\\Woo\\VariationService' => NullVariationService::class, 31 31 'Smartbuy\SMBCommerce\\Commerce\\Woo\\CouponService' => NullCouponService::class, 32 'Smartbuy\SMBCommerce\\Commerce\\Woo\\CartActionService' => NullCartActionService::class, 32 33 '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, 33 37 ]; 34 38 } … … 127 131 } 128 132 133 /** Bản Free: không có hành động giỏ nâng cao (gói Starter); thẻ giữ nút "Choose options". */ 134 class 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. */ 148 class 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). */ 166 class 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). */ 175 class 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 129 182 /** Bản Free: không có thông báo Telegram (gói Starter); lead vẫn được lưu bình thường. */ 130 183 class NullTelegramService { -
ninhle-ai-sales-assistant/trunk/src/PHP/Core/Uninstaller.php
r3752682 r3765552 54 54 wp_clear_scheduled_hook('smbcommerce_run_jobs'); 55 55 wp_clear_scheduled_hook('smbcommerce_finalize_outcomes'); 56 \Smartbuy\SMBCommerce\Insights\InsightsService::unschedule(); 56 57 57 58 /* … … 77 78 delete_option('smbcommerce_measurement_started_at'); 78 79 delete_option('smbcommerce_outcomes_watermark'); 80 delete_option('smbcommerce_ai_max_concurrency'); 81 delete_option('smbcommerce_async_mode'); 82 delete_option('smbcommerce_async_health'); 79 83 delete_transient('smbcommerce_missing_key_notice'); 84 delete_option('smbcommerce_review_prompt'); 85 delete_transient('smbcommerce_wp_ai_available'); 80 86 } 81 87 -
ninhle-ai-sales-assistant/trunk/src/PHP/Licensing/LicenseGate.php
r3752682 r3765552 45 45 // endpoint chỉ trả trường doanh thu khi can('revenue_attribution'). 46 46 '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, 47 53 48 54 // --- Pro --- -
ninhle-ai-sales-assistant/trunk/src/PHP/Privacy/PersonalData.php
r3752682 r3765552 185 185 } 186 186 187 $E = '\\Smartbuy\\SMBCommerce\\Analytics\\ValueEvents'; 187 188 $events = $wpdb->get_results($wpdb->prepare( 188 189 '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')) . ') 190 191 ORDER BY id ASC', 191 192 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], 193 196 $conversation_ids 194 197 ) … … 198 201 foreach ($events as $event) { 199 202 $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 } 201 224 $items[] = [ 202 225 'group_id' => 'smb-commerce-value', … … 205 228 'data' => [ 206 229 ['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, 210 231 ['name' => __('Recorded (UTC)', 'ninhle-ai-sales-assistant'), 'value' => $event['created_at']], 211 232 ], -
ninhle-ai-sales-assistant/trunk/src/PHP/REST/AdminController.php
r3761175 r3765552 109 109 } 110 110 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 111 139 public static function delete_conversation($request) { 112 140 if (!current_user_can('manage_smbcommerce')) { … … 170 198 $settings['smbcommerce_font_family'] = \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_font_family', 'system'); 171 199 $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); 172 205 173 206 $settings['smbcommerce_openai_key_exists'] = !empty(\Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_openai_key')); … … 232 265 $settings['smbcommerce_hide_on_checkout'] = (string) \Smartbuy\SMBCommerce\Core\SettingsManager::get_setting('smbcommerce_hide_on_checkout', '0'); 233 266 $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 } 234 274 235 275 return rest_ensure_response(['settings' => $settings]); … … 280 320 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_font_family', sanitize_text_field($params['smbcommerce_font_family'])); 281 321 } 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']); 284 326 } 285 327 … … 397 439 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_telegram_link', sanitize_text_field($params['smbcommerce_telegram_link'])); 398 440 } 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) { 400 442 if (isset($params[$toggle])) { 401 443 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting($toggle, (string) $params[$toggle] === '1' ? '1' : '0'); … … 405 447 \Smartbuy\SMBCommerce\Core\SettingsManager::set_setting('smbcommerce_ai_notice_text', sanitize_text_field($params['smbcommerce_ai_notice_text'])); 406 448 } 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(); 407 475 408 476 return rest_ensure_response(['success' => true]); -
ninhle-ai-sales-assistant/trunk/src/PHP/REST/ChatController.php
r3752682 r3765552 13 13 14 14 class 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 16 19 public static function bootstrap($request) { 17 20 $session_token = $request->get_header('X-SMB-Commerce-Session'); … … 40 43 $response['status'] = $conversation['status']; 41 44 $response['agent_name'] = self::agent_name($conversation); 45 $response['language'] = $conversation['language'] ?? null; 42 46 43 47 $messages = MessageRepository::get_messages($conversation['id'], 0, 50); … … 108 112 } 109 113 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 110 120 // 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. 111 121 \Smartbuy\SMBCommerce\Analytics\ValueEvents::record( … … 121 131 $is_thinking_mode = rest_sanitize_boolean($request->get_param('thinking_mode')); 122 132 123 \Smartbuy\SMBCommerce\Core\JobQueue::enqueue(\Smartbuy\SMBCommerce\Core\JobQueue::ACTION_PROCESS_MESSAGE,[133 $payload = [ 124 134 'conversation_id' => (int) $conversation_id, 125 135 '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 } 127 161 128 162 return rest_ensure_response($message); … … 145 179 } 146 180 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); 150 187 151 188 $messages = MessageRepository::get_messages($conversation_id, $last_message_id, 50); 152 189 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 } 155 203 156 204 return rest_ensure_response([ … … 160 208 'status' => $conversation['status'], 161 209 '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, 163 213 ]); 164 214 } … … 236 286 237 287 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 }); 239 291 240 292 \Smartbuy\SMBCommerce\Webhooks\WebhookDispatcher::send( … … 273 325 274 326 /** 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']; 276 328 277 329 /** … … 310 362 ['answer' => $type === 'resolution_yes' ? 'yes' : 'no'] 311 363 ); 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 } 312 368 return rest_ensure_response(['recorded' => $recorded]); 313 369 } … … 318 374 } 319 375 $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 320 390 $recorded = \Smartbuy\SMBCommerce\Analytics\ValueEvents::record( 321 391 \Smartbuy\SMBCommerce\Analytics\ValueEvents::QUALIFYING_INTERACTION, -
ninhle-ai-sales-assistant/trunk/src/PHP/REST/RouteRegistry.php
r3752682 r3765552 9 9 10 10 class RouteRegistry { 11 /** @var float|null Lúc handler của plugin bắt đầu, cho header Server-Timing. */ 12 private static $started = null; 13 11 14 public static function init() { 12 15 add_action('rest_api_init', [self::class, 'register_routes']); 16 add_filter('rest_request_before_callbacks', [self::class, 'start_timer'], 10, 3); 13 17 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; 14 25 } 15 26 … … 22 33 */ 23 34 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)) { 25 40 return $response; 26 41 } … … 32 47 // 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ọ. 33 48 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 } 34 57 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); 35 68 } 36 69 … … 70 103 ]); 71 104 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 72 118 register_rest_route($namespace, '/chat/clear', [ 73 119 'methods' => \WP_REST_Server::CREATABLE, … … 98 144 'type' => Guard::arg_text(true, 32), 99 145 '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], 100 148 ], 101 149 ]); … … 263 311 ]); 264 312 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 265 339 // Bảng "AI Value & Performance" (mọi gói; doanh thu kiểm quyền trong handler). 266 340 register_rest_route($namespace, '/admin/value-metrics', [ … … 317 391 '/tools/product-variations' => 'get_product_variations', 318 392 '/tools/check-coupon' => 'check_coupon', 393 '/tools/propose-cart-action' => 'propose_cart_action', 319 394 '/tools/order-status' => 'get_order_status', 395 '/tools/my-orders' => 'get_my_orders', 320 396 '/tools/customer-downloads' => 'get_customer_downloads', 321 397 '/tools/policy' => 'get_policy', -
ninhle-ai-sales-assistant/trunk/src/PHP/REST/ToolController.php
r3747521 r3765552 59 59 } 60 60 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 61 83 public static function get_product_detail($request) { 62 84 $service = new \Smartbuy\SMBCommerce\Commerce\Woo\ProductService(); … … 87 109 ]; 88 110 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; 92 116 } 93 117 … … 95 119 96 120 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)); 97 131 } 98 132 -
ninhle-ai-sales-assistant/trunk/src/PHP/Widget/WidgetLoader.php
r3761175 r3765552 63 63 'restUrl' => esc_url_raw(rest_url('smb-commerce/v1')), 64 64 '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(), 65 68 'widgetConfig' => self::public_config(), 66 69 // Dịch ở phía PHP để chuỗi vào được file .pot và theo locale WordPress. 67 70 'i18n' => [ 68 71 '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'), 69 74 'chatWindow' => __('Chat with our assistant', 'ninhle-ai-sales-assistant'), 70 75 'cart' => __('Cart', 'ninhle-ai-sales-assistant'), … … 109 114 'stageOrder' => __('Looking up your order…', 'ninhle-ai-sales-assistant'), 110 115 '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'), 111 154 'resolutionQuestion' => __('Did this answer your question?', 'ninhle-ai-sales-assistant'), 112 155 'yes' => __('Yes', 'ninhle-ai-sales-assistant'), … … 125 168 'voiceLanguage' => __('Your browser cannot recognise this language. Please type your message.', 'ninhle-ai-sales-assistant'), 126 169 '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(), 128 172 ]; 129 173 … … 160 204 // Giỏ hàng chỉ có khi WooCommerce đang chạy. 161 205 '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(), 162 213 // Thông báo "đang trò chuyện với AI": mặc định bật; rỗng = tắt. 163 214 'aiNotice' => $get('smbcommerce_ai_notice_enabled', '1') === '1' -
ninhle-ai-sales-assistant/trunk/src/PHP/WooCommerce/WooCommerceProductScanner.php
r3761175 r3765552 34 34 35 35 $synced_ids = []; 36 $fetched = 0; 36 37 $page = 1; 37 38 … … 52 53 53 54 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 } 54 61 $this->upsert_product($product, $table, $now); 55 62 $synced_ids[] = $product->get_id(); 56 63 } 57 64 65 $fetched += count($products); 58 66 $remaining -= count($products); 59 67 $page++; … … 64 72 } 65 73 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]; 68 78 $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) 71 81 )); 72 82 } … … 127 137 128 138 $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() : ''; 130 143 // 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 131 144 // 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. … … 215 228 $p['name'], 216 229 (!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']) . ')' : '', 218 231 $this->describe_stock($p['stock_status'], $p['stock_quantity'] ?? null), 219 232 $p['categories'] ?: 'Chưa phân loại', … … 227 240 $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"; 228 241 242 $this->context_ids = array_map(fn($p) => (int) $p['id'], $products); 229 243 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; 230 252 } 231 253 … … 252 274 AND (%s = '' OR search_text LIKE %s OR title LIKE %s OR sku LIKE %s) 253 275 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) 256 278 ORDER BY relevance DESC, total_sales DESC, cache_synced_at DESC 257 279 LIMIT %d", … … 274 296 ), ARRAY_A); 275 297 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 276 304 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) ?: []; 277 354 } 278 355 … … 313 390 'price' => $row['price'], 314 391 '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'], 316 394 // 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 317 395 // cho một size/màu cụ thể. -
ninhle-ai-sales-assistant/trunk/vite.config.ts
r3761175 r3765552 4 4 import { resolve } from 'path'; 5 5 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) 9 11 // Chạy: npm ci && npm run build → assets/dist/ 10 12 // build.ps1 chép assets/dist/.vite/manifest.json ra assets/dist/manifest.json khi đóng gói. … … 14 16 plugins: [ 15 17 preact({ 16 include: /widget \/.*\.tsx?$/,18 include: /widget(-pro)?\/.*\.tsx?$/, 17 19 }), 18 20 react({ … … 30 32 manifest: true, 31 33 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', 32 37 input: { 33 38 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') 35 43 }, 36 44 output: {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)