web-mojo 2.2.101 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +603 -0
  2. package/README.md +2 -2
  3. package/dist/admin-models.cjs.js +2 -0
  4. package/dist/admin-models.cjs.js.map +1 -0
  5. package/dist/admin-models.es.js +2 -0
  6. package/dist/admin-models.es.js.map +1 -0
  7. package/dist/admin.cjs.js +1 -1
  8. package/dist/admin.css +42 -0
  9. package/dist/admin.es.js +1 -1
  10. package/dist/auth.cjs.js +1 -1
  11. package/dist/auth.cjs.js.map +1 -1
  12. package/dist/auth.es.js +1 -1
  13. package/dist/auth.es.js.map +1 -1
  14. package/dist/charts.cjs.js +1 -1
  15. package/dist/charts.cjs.js.map +1 -1
  16. package/dist/charts.css +897 -1
  17. package/dist/charts.es.js +1 -1
  18. package/dist/charts.es.js.map +1 -1
  19. package/dist/chat.css +96 -0
  20. package/dist/chunks/AssistantPanelView-BG34Qbfj.js +2 -0
  21. package/dist/chunks/AssistantPanelView-BG34Qbfj.js.map +1 -0
  22. package/dist/chunks/AssistantPanelView-DCEV6VeI.js +2 -0
  23. package/dist/chunks/AssistantPanelView-DCEV6VeI.js.map +1 -0
  24. package/dist/chunks/ChatView-B73uox2v.js +2 -0
  25. package/dist/chunks/ChatView-B73uox2v.js.map +1 -0
  26. package/dist/chunks/ChatView-W8daOwIo.js +2 -0
  27. package/dist/chunks/ChatView-W8daOwIo.js.map +1 -0
  28. package/dist/chunks/Collection-BZlmtcuL.js +2 -0
  29. package/dist/chunks/Collection-BZlmtcuL.js.map +1 -0
  30. package/dist/chunks/Collection-Bwoq6muu.js +2 -0
  31. package/dist/chunks/Collection-Bwoq6muu.js.map +1 -0
  32. package/dist/chunks/ContextMenu-BPPtuqKk.js +2 -0
  33. package/dist/chunks/ContextMenu-BPPtuqKk.js.map +1 -0
  34. package/dist/chunks/ContextMenu-q76hjQb6.js +2 -0
  35. package/dist/chunks/ContextMenu-q76hjQb6.js.map +1 -0
  36. package/dist/chunks/DataView-BbrwHMV4.js +2 -0
  37. package/dist/chunks/{DataView-BFx2glFg.js.map → DataView-BbrwHMV4.js.map} +1 -1
  38. package/dist/chunks/DataView-k-7wmk5_.js +2 -0
  39. package/dist/chunks/{DataView-D5C_lDdg.js.map → DataView-k-7wmk5_.js.map} +1 -1
  40. package/dist/chunks/FormView-DPSuwWMq.js +3 -0
  41. package/dist/chunks/{FormView-e-PeRx1s.js.map → FormView-DPSuwWMq.js.map} +1 -1
  42. package/dist/chunks/FormView-Dcy7XOtC.js +3 -0
  43. package/dist/chunks/{FormView-Cu4iPfvU.js.map → FormView-Dcy7XOtC.js.map} +1 -1
  44. package/dist/chunks/ListView-DHC-yBIw.js +2 -0
  45. package/dist/chunks/ListView-DHC-yBIw.js.map +1 -0
  46. package/dist/chunks/ListView-iGBsD4a7.js +2 -0
  47. package/dist/chunks/ListView-iGBsD4a7.js.map +1 -0
  48. package/dist/chunks/MetricsCountryMapView-CAD9wR_T.js +2 -0
  49. package/dist/chunks/MetricsCountryMapView-CAD9wR_T.js.map +1 -0
  50. package/dist/chunks/MetricsCountryMapView-Dzk3Yrzx.js +2 -0
  51. package/dist/chunks/MetricsCountryMapView-Dzk3Yrzx.js.map +1 -0
  52. package/dist/chunks/Modal-DBJU16cc.js +3 -0
  53. package/dist/chunks/Modal-DBJU16cc.js.map +1 -0
  54. package/dist/chunks/Modal-DuULCMFZ.js +3 -0
  55. package/dist/chunks/Modal-DuULCMFZ.js.map +1 -0
  56. package/dist/chunks/Passkeys-CGRZ8ZMv.js +2 -0
  57. package/dist/chunks/{Passkeys-DfVHrRPY.js.map → Passkeys-CGRZ8ZMv.js.map} +1 -1
  58. package/dist/chunks/Passkeys-Dr8-oSm9.js +2 -0
  59. package/dist/chunks/{Passkeys-Bj-ufmei.js.map → Passkeys-Dr8-oSm9.js.map} +1 -1
  60. package/dist/chunks/TokenManager-CcQFvaFD.js +2 -0
  61. package/dist/chunks/TokenManager-CcQFvaFD.js.map +1 -0
  62. package/dist/chunks/TokenManager-DEWZqbuo.js +2 -0
  63. package/dist/chunks/TokenManager-DEWZqbuo.js.map +1 -0
  64. package/dist/chunks/User-DNQhdBtI.js +2 -0
  65. package/dist/chunks/User-DNQhdBtI.js.map +1 -0
  66. package/dist/chunks/User-Dg7xpYEI.js +2 -0
  67. package/dist/chunks/User-Dg7xpYEI.js.map +1 -0
  68. package/dist/chunks/UserProfileView-B5nczdfw.js +2 -0
  69. package/dist/chunks/UserProfileView-B5nczdfw.js.map +1 -0
  70. package/dist/chunks/UserProfileView-Bpz3VZmP.js +2 -0
  71. package/dist/chunks/UserProfileView-Bpz3VZmP.js.map +1 -0
  72. package/dist/chunks/View-C5n3sIFi.js +2 -0
  73. package/dist/chunks/View-C5n3sIFi.js.map +1 -0
  74. package/dist/chunks/View-Yazho7OL.js +2 -0
  75. package/dist/chunks/View-Yazho7OL.js.map +1 -0
  76. package/dist/chunks/WebApp-CeiDNV6L.js +2 -0
  77. package/dist/chunks/WebApp-CeiDNV6L.js.map +1 -0
  78. package/dist/chunks/WebApp-irKlhuFX.js +2 -0
  79. package/dist/chunks/WebApp-irKlhuFX.js.map +1 -0
  80. package/dist/chunks/admin-B5tf0zOO.js +2 -0
  81. package/dist/chunks/admin-B5tf0zOO.js.map +1 -0
  82. package/dist/chunks/admin-CGoTpXfs.js +2 -0
  83. package/dist/chunks/admin-CGoTpXfs.js.map +1 -0
  84. package/dist/chunks/exportChart-BTJBYkOz.js +2 -0
  85. package/dist/chunks/exportChart-BTJBYkOz.js.map +1 -0
  86. package/dist/chunks/exportChart-kQ-we4Cp.js +2 -0
  87. package/dist/chunks/exportChart-kQ-we4Cp.js.map +1 -0
  88. package/dist/chunks/index-BNjCQA7q.js +2 -0
  89. package/dist/chunks/{index-BY54viKF.js.map → index-BNjCQA7q.js.map} +1 -1
  90. package/dist/chunks/index-DBsIDOAa.js +2 -0
  91. package/dist/chunks/{index-Df5lx5TH.js.map → index-DBsIDOAa.js.map} +1 -1
  92. package/dist/chunks/version-BURwX10Q.js +2 -0
  93. package/dist/chunks/version-BURwX10Q.js.map +1 -0
  94. package/dist/chunks/version-CYGIXntv.js +2 -0
  95. package/dist/chunks/version-CYGIXntv.js.map +1 -0
  96. package/dist/core.css +306 -0
  97. package/dist/css/web-mojo.css +1 -1
  98. package/dist/docit.cjs.js +1 -1
  99. package/dist/docit.cjs.js.map +1 -1
  100. package/dist/docit.es.js +1 -1
  101. package/dist/docit.es.js.map +1 -1
  102. package/dist/index.cjs.js +1 -1
  103. package/dist/index.cjs.js.map +1 -1
  104. package/dist/index.es.js +1 -1
  105. package/dist/index.es.js.map +1 -1
  106. package/dist/lightbox.cjs.js +1 -1
  107. package/dist/lightbox.cjs.js.map +1 -1
  108. package/dist/lightbox.es.js +1 -1
  109. package/dist/lightbox.es.js.map +1 -1
  110. package/dist/map.cjs.js +1 -1
  111. package/dist/map.cjs.js.map +1 -1
  112. package/dist/map.es.js +1 -1
  113. package/dist/map.es.js.map +1 -1
  114. package/dist/mojo-auth.es.js +94 -65
  115. package/dist/mojo-auth.umd.js +1 -1
  116. package/dist/portal.css +86 -0
  117. package/dist/timeline.cjs.js +1 -1
  118. package/dist/timeline.cjs.js.map +1 -1
  119. package/dist/timeline.es.js +1 -1
  120. package/dist/timeline.es.js.map +1 -1
  121. package/dist/user-profile.cjs.js +1 -1
  122. package/dist/user-profile.es.js +1 -1
  123. package/dist/web-mojo.lite.iife.js +4183 -3916
  124. package/dist/web-mojo.lite.iife.js.map +1 -1
  125. package/dist/web-mojo.lite.iife.min.js +289 -311
  126. package/dist/web-mojo.lite.iife.min.js.map +1 -1
  127. package/package.json +7 -2
  128. package/dist/chunks/AssistantPanelView-Bdpmd4z7.js +0 -2
  129. package/dist/chunks/AssistantPanelView-Bdpmd4z7.js.map +0 -1
  130. package/dist/chunks/AssistantPanelView-C0bdbEWr.js +0 -2
  131. package/dist/chunks/AssistantPanelView-C0bdbEWr.js.map +0 -1
  132. package/dist/chunks/ChatView-B1MZNPGy.js +0 -2
  133. package/dist/chunks/ChatView-B1MZNPGy.js.map +0 -1
  134. package/dist/chunks/ChatView-DXl9nVVV.js +0 -2
  135. package/dist/chunks/ChatView-DXl9nVVV.js.map +0 -1
  136. package/dist/chunks/Collection-C39Oy2q0.js +0 -2
  137. package/dist/chunks/Collection-C39Oy2q0.js.map +0 -1
  138. package/dist/chunks/Collection-CyK0u557.js +0 -2
  139. package/dist/chunks/Collection-CyK0u557.js.map +0 -1
  140. package/dist/chunks/ContextMenu-D-C7V6J6.js +0 -2
  141. package/dist/chunks/ContextMenu-D-C7V6J6.js.map +0 -1
  142. package/dist/chunks/ContextMenu-hjqR1pGW.js +0 -2
  143. package/dist/chunks/ContextMenu-hjqR1pGW.js.map +0 -1
  144. package/dist/chunks/DataView-BFx2glFg.js +0 -2
  145. package/dist/chunks/DataView-D5C_lDdg.js +0 -2
  146. package/dist/chunks/Dialog-1umNJi4B.js +0 -3
  147. package/dist/chunks/Dialog-1umNJi4B.js.map +0 -1
  148. package/dist/chunks/Dialog-BnxWLMfr.js +0 -3
  149. package/dist/chunks/Dialog-BnxWLMfr.js.map +0 -1
  150. package/dist/chunks/FormView-Cu4iPfvU.js +0 -3
  151. package/dist/chunks/FormView-e-PeRx1s.js +0 -3
  152. package/dist/chunks/ListView-D_hOtfWZ.js +0 -2
  153. package/dist/chunks/ListView-D_hOtfWZ.js.map +0 -1
  154. package/dist/chunks/ListView-Jkke6pU1.js +0 -2
  155. package/dist/chunks/ListView-Jkke6pU1.js.map +0 -1
  156. package/dist/chunks/MetricsCountryMapView-UdvJWArx.js +0 -2
  157. package/dist/chunks/MetricsCountryMapView-UdvJWArx.js.map +0 -1
  158. package/dist/chunks/MetricsCountryMapView-jLWCL6Hm.js +0 -2
  159. package/dist/chunks/MetricsCountryMapView-jLWCL6Hm.js.map +0 -1
  160. package/dist/chunks/MetricsMiniChartWidget-BaXxR9C6.js +0 -2
  161. package/dist/chunks/MetricsMiniChartWidget-BaXxR9C6.js.map +0 -1
  162. package/dist/chunks/MetricsMiniChartWidget-DSKmKY2Z.js +0 -2
  163. package/dist/chunks/MetricsMiniChartWidget-DSKmKY2Z.js.map +0 -1
  164. package/dist/chunks/MiniPieChart-B3sM4Bjv.js +0 -2
  165. package/dist/chunks/MiniPieChart-B3sM4Bjv.js.map +0 -1
  166. package/dist/chunks/MiniPieChart-DL5Rynvm.js +0 -2
  167. package/dist/chunks/MiniPieChart-DL5Rynvm.js.map +0 -1
  168. package/dist/chunks/MiniSeriesChart-C4DPVbU_.js +0 -2
  169. package/dist/chunks/MiniSeriesChart-C4DPVbU_.js.map +0 -1
  170. package/dist/chunks/MiniSeriesChart-DdNMLwfh.js +0 -2
  171. package/dist/chunks/MiniSeriesChart-DdNMLwfh.js.map +0 -1
  172. package/dist/chunks/Modal-COT4zRh3.js +0 -2
  173. package/dist/chunks/Modal-COT4zRh3.js.map +0 -1
  174. package/dist/chunks/Modal-DgMMvDnw.js +0 -2
  175. package/dist/chunks/Modal-DgMMvDnw.js.map +0 -1
  176. package/dist/chunks/Passkeys-Bj-ufmei.js +0 -2
  177. package/dist/chunks/Passkeys-DfVHrRPY.js +0 -2
  178. package/dist/chunks/TokenManager-DIEaBGJh.js +0 -2
  179. package/dist/chunks/TokenManager-DIEaBGJh.js.map +0 -1
  180. package/dist/chunks/TokenManager-DeXkJy0u.js +0 -2
  181. package/dist/chunks/TokenManager-DeXkJy0u.js.map +0 -1
  182. package/dist/chunks/UserProfileView-B_jyMUp0.js +0 -2
  183. package/dist/chunks/UserProfileView-B_jyMUp0.js.map +0 -1
  184. package/dist/chunks/UserProfileView-DcpvvGQS.js +0 -2
  185. package/dist/chunks/UserProfileView-DcpvvGQS.js.map +0 -1
  186. package/dist/chunks/WebApp-BvFnKj-I.js +0 -2
  187. package/dist/chunks/WebApp-BvFnKj-I.js.map +0 -1
  188. package/dist/chunks/WebApp-CfDWKmwH.js +0 -2
  189. package/dist/chunks/WebApp-CfDWKmwH.js.map +0 -1
  190. package/dist/chunks/WebSocketClient-BQAZr8C4.js +0 -2
  191. package/dist/chunks/WebSocketClient-BQAZr8C4.js.map +0 -1
  192. package/dist/chunks/WebSocketClient-YR5d82h8.js +0 -2
  193. package/dist/chunks/WebSocketClient-YR5d82h8.js.map +0 -1
  194. package/dist/chunks/admin-BgvcCWQp.js +0 -2
  195. package/dist/chunks/admin-BgvcCWQp.js.map +0 -1
  196. package/dist/chunks/admin-DzhyZ_Tn.js +0 -2
  197. package/dist/chunks/admin-DzhyZ_Tn.js.map +0 -1
  198. package/dist/chunks/index-BY54viKF.js +0 -2
  199. package/dist/chunks/index-Df5lx5TH.js +0 -2
  200. package/dist/chunks/version-8mBBYRDe.js +0 -2
  201. package/dist/chunks/version-8mBBYRDe.js.map +0 -1
  202. package/dist/chunks/version-CoLHxZIU.js +0 -2
  203. package/dist/chunks/version-CoLHxZIU.js.map +0 -1
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./View-Yazho7OL.js"),s=require("./ChatView-B73uox2v.js"),e=require("../admin-models.cjs.js"),n=require("./admin-CGoTpXfs.js");class AssistantPanelView extends t.View{constructor(t={}){super({className:"assistant-panel-view",...t}),this.app=t.app,this.ws=this.app?.ws,this.conversationId=t.conversationId||this.app?._assistantConversationId||null,this._wsHandlers={},this._messageIdCounter=0,this._hasMessages=!1,this._activePlans={},this._requestStartTime=null,this._showingHistory=!1}getTemplate(){return`\n <div class="assistant-panel-resize-handle" data-ref="resize-handle"></div>\n <div class="assistant-panel-layout">\n <div class="assistant-panel-header">\n <button class="assistant-panel-header-btn" data-action="toggle-history" type="button" title="Conversation history">\n <i class="bi bi-list"></i>\n </button>\n <span class="assistant-panel-title text-truncate" data-ref="panel-title">New conversation</span>\n <div class="d-flex gap-1 ms-auto">\n <button class="assistant-panel-header-btn" data-action="new-conversation" type="button" title="New conversation">\n <i class="bi bi-plus-lg"></i>\n </button>\n <button class="assistant-panel-header-btn" data-action="fullscreen" type="button" title="Open fullscreen">\n <i class="bi bi-arrows-fullscreen"></i>\n </button>\n <button class="assistant-panel-header-btn" data-action="pop-out" type="button" title="Open in popup window">\n <i class="bi bi-box-arrow-up-right"></i>\n </button>\n <button class="assistant-panel-header-btn" data-action="close-panel" type="button" title="Close">\n <i class="bi bi-x-lg"></i>\n </button>\n </div>\n </div>\n\n <div class="assistant-panel-history d-none" data-ref="history" data-container="conversation-list"></div>\n\n <div class="assistant-panel-chat" data-ref="chat-wrapper">\n <div class="assistant-welcome" data-ref="welcome">\n <div class="assistant-welcome-content">\n <div class="assistant-welcome-icon">\n <i class="bi bi-stars"></i>\n </div>\n <h3 class="assistant-welcome-title">Hi ${this._escapeHtml(this.app?.activeUser?.get("first_name")||"there")}</h3>\n <p class="assistant-welcome-subtitle">How can I help you today?</p>\n <div class="assistant-suggestions">\n <button class="assistant-suggestion" data-action="use-suggestion" data-text="Show me a summary of recent activity">\n <i class="bi bi-activity"></i>\n <span>Recent activity summary</span>\n </button>\n <button class="assistant-suggestion" data-action="use-suggestion" data-text="How many active users are there?">\n <i class="bi bi-people"></i>\n <span>Active user count</span>\n </button>\n </div>\n </div>\n </div>\n <div class="assistant-chat-area" data-container="chat-area"></div>\n <div class="assistant-input-wrapper">\n <div class="assistant-input-status d-none" data-ref="input-status"></div>\n <div class="assistant-input-box">\n <textarea class="assistant-input" placeholder="Message the assistant..." rows="1" data-ref="input"></textarea>\n <button class="assistant-send-btn" data-action="send" type="button" title="Send message" data-ref="send-btn">\n <i class="bi bi-arrow-up"></i>\n </button>\n <button class="assistant-stop-btn d-none" data-action="stop" type="button" title="Stop generating" data-ref="stop-btn">\n <i class="bi bi-stop-fill"></i>\n </button>\n </div>\n <div class="assistant-input-footer">\n <span class="assistant-connection-indicator" data-ref="status">\n <span class="status-dot connected"></span>\n </span>\n <span class="text-muted">Enter to send</span>\n </div>\n </div>\n </div>\n </div>\n `}async onInit(){this.conversations=new e.AssistantConversationList,this.conversations.params.user=this.app?.activeUser?.id,this.conversationListView=new n.AssistantConversationListView({containerId:"conversation-list",collection:this.conversations}),this.addChild(this.conversationListView),this.chatView=new s.ChatView({containerId:"chat-area",theme:"compact",messageViewClass:n.AssistantMessageView,currentUserId:this.app?.activeUser?.id,showFileInput:!1,showInput:!1,adapter:this._createAdapter()}),this.addChild(this.chatView);const t=this.chatView.addMessage.bind(this.chatView);this.chatView.addMessage=(s,e)=>{t(s,e),"assistant"===s.role&&(s.content||s.blocks?.length)&&(this.chatView.hideThinking(),this._setInputEnabled(!0))},this.conversationListView.on("conversation:select",t=>{this._onConversationSelect(t),this._toggleHistory(!1)}),this.conversationListView.on("conversation:new",()=>{this._onNewConversation(),this._toggleHistory(!1)}),this.conversationListView.on("conversation:deleted",t=>this._onConversationDeleted(t)),this._subscribeWS()}async onAfterRender(){await super.onAfterRender();const t=this.element.querySelector('[data-ref="input"]');t&&(t.addEventListener("input",()=>this._autoResize(t)),t.addEventListener("keydown",t=>this._handleKeydown(t)),setTimeout(()=>t.focus(),100)),this.conversationId&&(this._showChatArea(),await this.chatView.refresh()),this._updateConnectionStatus(),this._updateTitle(),this._setupResizeHandle()}_setupResizeHandle(){const t=this.element?.querySelector('[data-ref="resize-handle"]');if(!t)return;const s="mojo:assistant_panel_width",e=localStorage.getItem(s);if(e){const t=parseInt(e,10);if(t>=300&&t<=700){const s=document.getElementById("assistant-panel");s&&(s.style.width=t+"px")}}let n,i;const a=t=>{const s=n-t.clientX,e=Math.min(700,Math.max(300,i+s)),a=document.getElementById("assistant-panel");a&&(a.style.width=e+"px")},o=()=>{document.removeEventListener("mousemove",a),document.removeEventListener("mouseup",o),document.body.style.cursor="",document.body.style.userSelect="";const t=document.getElementById("assistant-panel");t&&localStorage.setItem(s,parseInt(t.style.width,10))};t.addEventListener("mousedown",t=>{t.preventDefault(),n=t.clientX;const s=document.getElementById("assistant-panel");i=s?s.offsetWidth:500,document.body.style.cursor="col-resize",document.body.style.userSelect="none",document.addEventListener("mousemove",a),document.addEventListener("mouseup",o)})}onActionToggleHistory(){this._toggleHistory(!this._showingHistory)}onActionNewConversation(){this._onNewConversation(),this._showingHistory&&this._toggleHistory(!1)}onActionClosePanel(){this.emit("panel:close")}onActionFullscreen(){this.emit("panel:fullscreen",{conversationId:this.conversationId})}onActionPopOut(){this.emit("panel:popout",{conversationId:this.conversationId})}onActionUseSuggestion(t,s){const e=s.dataset.text||s.closest("[data-text]")?.dataset.text;if(!e)return;const n=this.element.querySelector('[data-ref="input"]');n&&(n.value=e,this._autoResize(n)),this._sendMessage()}onActionSend(){this._sendMessage()}onActionStop(){this.chatView.hideThinking(),this._setInputEnabled(!0),this._showSystemMessage("Response cancelled.");const t=this.element?.querySelector('[data-ref="input"]');t&&t.focus()}_toggleHistory(t){this._showingHistory=t;const s=this.element?.querySelector('[data-ref="history"]'),e=this.element?.querySelector('[data-ref="chat-wrapper"]'),n=this.element?.querySelector('[data-action="toggle-history"] i');s&&s.classList.toggle("d-none",!t),e&&e.classList.toggle("d-none",t),n&&(n.className=t?"bi bi-chat-dots":"bi bi-list"),t&&this.conversationListView.refresh()}_autoResize(t){t.style.height="auto",t.style.height=Math.min(t.scrollHeight,200)+"px"}_handleKeydown(t){"Enter"!==t.key||t.shiftKey||(t.preventDefault(),this._sendMessage())}async _sendMessage(){const t=this.element.querySelector('[data-ref="input"]');if(!t)return;const s=t.value.trim();s&&(t.value="",t.style.height="auto",this._showChatArea(),await this.chatView.adapter.addNote({text:s,files:[]}))}_showChatArea(){if(this._hasMessages)return;this._hasMessages=!0;const t=this.element.querySelector('[data-ref="welcome"]'),s=this.element.querySelector('[data-container="chat-area"]');t&&t.classList.add("d-none"),s&&s.classList.remove("d-none")}_showWelcome(){this._hasMessages=!1;const t=this.element.querySelector('[data-ref="welcome"]'),s=this.element.querySelector('[data-container="chat-area"]');t&&t.classList.remove("d-none"),s&&s.classList.add("d-none")}_setInputEnabled(t,s){const e=this.element?.querySelector('[data-ref="input"]'),n=this.element?.querySelector('[data-ref="send-btn"]'),i=this.element?.querySelector('[data-ref="stop-btn"]');e&&(e.disabled=!t),n&&n.classList.toggle("d-none",!t),i&&i.classList.toggle("d-none",t),this._setInputStatus(t?null:s),this._responseTimeout&&clearTimeout(this._responseTimeout),t?this._requestStartTime=null:this._responseTimeout=setTimeout(()=>this._onResponseTimeout(),6e4)}_setInputStatus(t){const s=this.element?.querySelector('[data-ref="input-status"]');s&&(t?(s.innerHTML=`${this._escapeHtml(t)} <span class="assistant-input-status-dismiss">Click to dismiss</span>`,s.classList.remove("d-none"),s._hasDismiss||(s._hasDismiss=!0,s.addEventListener("click",()=>{this.chatView.hideThinking(),this._setInputEnabled(!0);const t=this.element?.querySelector('[data-ref="input"]');t&&t.focus()}))):(s.classList.add("d-none"),s.innerHTML=""))}_onResponseTimeout(){this._responseTimeout=null,this.chatView.hideThinking(),this._setInputEnabled(!0),this._showSystemMessage("Request timed out. Please try again.")}_updateTitle(t){const s=this.element?.querySelector('[data-ref="panel-title"]');s&&(s.textContent=t||(this.conversationId?"Assistant":"New conversation"))}_createAdapter(){return{fetch:async()=>{if(!this.conversationId)return[];try{const t=new e.Assistant({id:this.conversationId});await t.fetch({graph:"detail"});const s=t.get("title")||t.get("summary");s&&this._updateTitle(s);const i=(t.get("messages")||[]).map(t=>this._transformMessage(t)).filter(Boolean);return n.AssistantView._collapseMessages(i)}catch(t){return 404===t.status&&(this._onNewConversation(),this._showSystemMessage("Conversation not found.")),[]}},addNote:async t=>{if(!t.text||!t.text.trim())return{success:!1};const s={id:"local-"+ ++this._messageIdCounter,role:"user",author:{id:this.app?.activeUser?.id,name:this.app?.activeUser?.get("display_name")||"You"},content:t.text,timestamp:/* @__PURE__ */(new Date).toISOString()};if(this.chatView.addMessage(s),this.chatView.showThinking("Thinking..."),this._requestStartTime=Date.now(),this._setInputEnabled(!1,"Waiting for response…"),this.ws&&this.ws.isConnected)this.ws.send({type:"assistant_message",message:t.text,conversation_id:this.conversationId});else try{const s=await this.app.rest.post("/api/assistant",{message:t.text,conversation_id:this.conversationId}),e=s?.data?.data||s?.data||s;e.conversation_id&&(this.conversationId=e.conversation_id,this.app._assistantConversationId=this.conversationId),e.response&&this.chatView.addMessage(this._transformMessage(e.response)),this._setInputEnabled(!0)}catch(e){this._handleAPIError(e)}return{success:!0}}}}_subscribeWS(){this.ws&&(this._wsHandlers={thinking:t=>this._onThinking(t),text:t=>this._onText(t),tool_call:t=>this._onToolCall(t),response:t=>this._onResponse(t),error:t=>this._onError(t),plan:t=>this._onPlan(t),plan_update:t=>this._onPlanUpdate(t),message:t=>this._dispatchWSMessage(t),connected:()=>this._updateConnectionStatus(),disconnected:()=>this._updateConnectionStatus(),reconnecting:()=>this._updateConnectionStatus()},this.ws.on("message:assistant_thinking",this._wsHandlers.thinking),this.ws.on("message:assistant_text",this._wsHandlers.text),this.ws.on("message:assistant_tool_call",this._wsHandlers.tool_call),this.ws.on("message:assistant_response",this._wsHandlers.response),this.ws.on("message:assistant_error",this._wsHandlers.error),this.ws.on("message:assistant_plan",this._wsHandlers.plan),this.ws.on("message:assistant_plan_update",this._wsHandlers.plan_update),this.ws.on("message:message",this._wsHandlers.message),this.ws.on("connected",this._wsHandlers.connected),this.ws.on("disconnected",this._wsHandlers.disconnected),this.ws.on("reconnecting",this._wsHandlers.reconnecting))}_unsubscribeWS(){this.ws&&this._wsHandlers&&(this.ws.off("message:assistant_thinking",this._wsHandlers.thinking),this.ws.off("message:assistant_text",this._wsHandlers.text),this.ws.off("message:assistant_tool_call",this._wsHandlers.tool_call),this.ws.off("message:assistant_response",this._wsHandlers.response),this.ws.off("message:assistant_error",this._wsHandlers.error),this.ws.off("message:assistant_plan",this._wsHandlers.plan),this.ws.off("message:assistant_plan_update",this._wsHandlers.plan_update),this.ws.off("message:message",this._wsHandlers.message),this.ws.off("connected",this._wsHandlers.connected),this.ws.off("disconnected",this._wsHandlers.disconnected),this.ws.off("reconnecting",this._wsHandlers.reconnecting),this._wsHandlers={})}_dispatchWSMessage(t){const s=t?.data;if(s?.type)switch(s.type){case"assistant_thinking":this._onThinking(s);break;case"assistant_text":this._onText(s);break;case"assistant_tool_call":this._onToolCall(s);break;case"assistant_response":this._onResponse(s);break;case"assistant_error":this._onError(s);break;case"assistant_plan":this._onPlan(s);break;case"assistant_plan_update":this._onPlanUpdate(s)}}_isMyConversation(t){return!t.conversation_id||!this.conversationId||String(t.conversation_id)===String(this.conversationId)}_adoptConversationId(t){t.conversation_id&&!this.conversationId&&(this.conversationId=t.conversation_id,this.app._assistantConversationId=this.conversationId)}_onThinking(t){this._isMyConversation(t)&&(this._adoptConversationId(t),this._showChatArea(),this.chatView.showThinking("Thinking..."),this._setInputEnabled(!1,"Assistant is thinking…"))}_onText(t){if(!this._isMyConversation(t))return;this._adoptConversationId(t),this._resetResponseTimeout();const s=this._transformMessage({id:t.message_id||"text-"+ ++this._messageIdCounter,role:"assistant",content:t.text||"",blocks:t.blocks||[],tool_calls:[],created:t.created||t.timestamp||/* @__PURE__ */(new Date).toISOString()});s&&(s.content||s.blocks?.length)&&this.chatView.addMessage(s)}_onToolCall(t){this._isMyConversation(t)&&(this.chatView.showThinking(`Using ${t.tool||t.name||"tool"}...`),this._resetResponseTimeout())}_resetResponseTimeout(){if(this._responseTimeout){if(this._requestStartTime&&Date.now()-this._requestStartTime>=3e5)return void this._onResponseTimeout();clearTimeout(this._responseTimeout),this._responseTimeout=setTimeout(()=>this._onResponseTimeout(),6e4)}}_onResponse(t){if(!this._isMyConversation(t))return;this.chatView.hideThinking(),this._setInputEnabled(!0),this._adoptConversationId(t);const s=this.element?.querySelector('[data-ref="input"]');s&&s.focus();const e=this._transformMessage({id:t.message_id||"resp-"+ ++this._messageIdCounter,role:"assistant",content:t.response||t.content||t.message||"",blocks:t.blocks||[],tool_calls:t.tool_calls_made||t.tool_calls||[],created:t.created||t.timestamp||/* @__PURE__ */(new Date).toISOString()});e&&(e.content||e.blocks?.length||e.tool_calls?.length)&&this.chatView.addMessage(e)}_onError(t){if(!this._isMyConversation(t))return;this.chatView.hideThinking(),this._setInputEnabled(!0),this._adoptConversationId(t);const s=t.error||t.message||"An error occurred";this._showSystemMessage(s)}_onPlan(t){if(!this._isMyConversation(t))return;this._adoptConversationId(t),this._showChatArea();const s=t.plan;s&&(this._activePlans[s.plan_id]=s,this.chatView.addMessage({id:`plan-${s.plan_id}`,role:"assistant",author:{name:"Assistant"},content:"",timestamp:/* @__PURE__ */(new Date).toISOString(),blocks:[{type:"progress",...s}],tool_calls:[]}))}_onPlanUpdate(t){if(!this._isMyConversation(t))return;const s=this._activePlans[t.plan_id];if(s){const e=s.steps.find(s=>s.id===t.step_id);e&&(e.status=t.status,e.summary=t.summary)}const e=this.chatView.messageViews.get(`plan-${t.plan_id}`);e?.updateProgressStep&&e.updateProgressStep(t.plan_id,t.step_id,t.status,t.summary),this._resetResponseTimeout()}async _onConversationSelect(t){this.conversationId=t.id,this.app._assistantConversationId=this.conversationId,this.conversationListView.setActive(t.id),this._showChatArea(),this._updateTitle(t.model?.get("title")||t.model?.get("summary")),await this.chatView.refresh()}_onNewConversation(){this.conversationId=null,this.app._assistantConversationId=null,this.conversationListView.setActive(null),this.chatView.clearMessages(),this._setInputEnabled(!0),this._showWelcome(),this._updateTitle();const t=this.element?.querySelector('[data-ref="input"]');t&&t.focus()}_onConversationDeleted(t){String(t.id)===String(this.conversationId)&&this._onNewConversation()}_transformMessage(t){if("tool_result"===t.role)return null;let s=t.content||t.text||"",e=t.blocks||[],i=t.tool_calls||[];if(i.length>0){i=i.map(t=>!t.type&&t.tool?{type:"tool_use",name:t.tool,input:t.input}:t);const t=i.filter(t=>"text"===t.type&&t.text).map(t=>t.text);!s&&t.length>0&&(s=t.join("\n\n")),i=i.filter(t=>"tool_use"===t.type).filter(t=>!n.AssistantView.INTERNAL_TOOLS.has(t.name))}if(0===e.length&&s.includes("assistant_block")){const t=n.AssistantView._parseBlocks(s);s=t.content,e=t.blocks}const a=this.app?.activeUser?.id;return{id:t.id,role:t.role||"user",author:"assistant"===t.role?{name:"Assistant"}:t.author||{name:t.user?.display_name||this.app?.activeUser?.get("display_name")||"You",id:t.user?.id||a},content:s,timestamp:t.created||t.timestamp,blocks:e,tool_calls:i,_conversationId:this.conversationId}}_showSystemMessage(t){this._showChatArea(),this.chatView.addMessage({id:"sys-"+ ++this._messageIdCounter,type:"system_event",content:t,timestamp:/* @__PURE__ */(new Date).toISOString()})}_handleAPIError(t){404===t.status?this._showSystemMessage("Assistant is not enabled on this server."):503===t.status?this._showSystemMessage("LLM API key not configured. Contact your administrator."):this._showSystemMessage("Failed to send message. Please try again."),this._setInputEnabled(!0)}_updateConnectionStatus(){const t=this.element?.querySelector(".status-dot");t&&(this.ws?.isConnected?(t.className="status-dot connected",t.title="Connected",this._responseTimeout?this._setInputEnabled(!1,"Waiting for response…"):this._setInputEnabled(!0)):this.ws?.isReconnecting?(t.className="status-dot reconnecting",t.title="Reconnecting...",this._setInputEnabled(!1,"Reconnecting…"),this._responseTimeout&&(clearTimeout(this._responseTimeout),this._responseTimeout=null)):(t.className="status-dot disconnected",t.title="Disconnected",this._setInputEnabled(!1,"Disconnected — reconnecting…"),this._responseTimeout&&(clearTimeout(this._responseTimeout),this._responseTimeout=null)))}_escapeHtml(t){const s=document.createElement("div");return s.textContent=t,s.innerHTML}focusInput(){const t=this.element?.querySelector('[data-ref="input"]');t&&t.focus()}async onBeforeDestroy(){this._unsubscribeWS(),this._responseTimeout&&(clearTimeout(this._responseTimeout),this._responseTimeout=null)}}exports.default=AssistantPanelView;
2
+ //# sourceMappingURL=AssistantPanelView-DCEV6VeI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssistantPanelView-DCEV6VeI.js","sources":["../../src/extensions/admin/assistant/AssistantPanelView.js"],"sourcesContent":["import View from '@core/View.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport { AssistantConversation, AssistantConversationList } from '@ext/admin/models/Assistant.js';\nimport AssistantMessageView from './AssistantMessageView.js';\nimport AssistantConversationListView from './AssistantConversationListView.js';\nimport AssistantView from './AssistantView.js';\n\n/**\n * AssistantPanelView - Chat-only sidebar panel for the admin assistant\n *\n * Compact layout for the right sidebar panel. No conversation list by default —\n * a hamburger toggle switches between chat and conversation history.\n *\n * Emits:\n * panel:close — when the close button is clicked\n */\nclass AssistantPanelView extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-panel-view',\n ...options\n });\n\n this.app = options.app;\n this.ws = this.app?.ws;\n this.conversationId = options.conversationId || this.app?._assistantConversationId || null;\n this._wsHandlers = {};\n this._messageIdCounter = 0;\n this._hasMessages = false;\n this._activePlans = {};\n this._requestStartTime = null;\n this._showingHistory = false;\n }\n\n getTemplate() {\n const userName = this._escapeHtml(\n this.app?.activeUser?.get('first_name') || 'there'\n );\n\n return `\n <div class=\"assistant-panel-resize-handle\" data-ref=\"resize-handle\"></div>\n <div class=\"assistant-panel-layout\">\n <div class=\"assistant-panel-header\">\n <button class=\"assistant-panel-header-btn\" data-action=\"toggle-history\" type=\"button\" title=\"Conversation history\">\n <i class=\"bi bi-list\"></i>\n </button>\n <span class=\"assistant-panel-title text-truncate\" data-ref=\"panel-title\">New conversation</span>\n <div class=\"d-flex gap-1 ms-auto\">\n <button class=\"assistant-panel-header-btn\" data-action=\"new-conversation\" type=\"button\" title=\"New conversation\">\n <i class=\"bi bi-plus-lg\"></i>\n </button>\n <button class=\"assistant-panel-header-btn\" data-action=\"fullscreen\" type=\"button\" title=\"Open fullscreen\">\n <i class=\"bi bi-arrows-fullscreen\"></i>\n </button>\n <button class=\"assistant-panel-header-btn\" data-action=\"pop-out\" type=\"button\" title=\"Open in popup window\">\n <i class=\"bi bi-box-arrow-up-right\"></i>\n </button>\n <button class=\"assistant-panel-header-btn\" data-action=\"close-panel\" type=\"button\" title=\"Close\">\n <i class=\"bi bi-x-lg\"></i>\n </button>\n </div>\n </div>\n\n <div class=\"assistant-panel-history d-none\" data-ref=\"history\" data-container=\"conversation-list\"></div>\n\n <div class=\"assistant-panel-chat\" data-ref=\"chat-wrapper\">\n <div class=\"assistant-welcome\" data-ref=\"welcome\">\n <div class=\"assistant-welcome-content\">\n <div class=\"assistant-welcome-icon\">\n <i class=\"bi bi-stars\"></i>\n </div>\n <h3 class=\"assistant-welcome-title\">Hi ${userName}</h3>\n <p class=\"assistant-welcome-subtitle\">How can I help you today?</p>\n <div class=\"assistant-suggestions\">\n <button class=\"assistant-suggestion\" data-action=\"use-suggestion\" data-text=\"Show me a summary of recent activity\">\n <i class=\"bi bi-activity\"></i>\n <span>Recent activity summary</span>\n </button>\n <button class=\"assistant-suggestion\" data-action=\"use-suggestion\" data-text=\"How many active users are there?\">\n <i class=\"bi bi-people\"></i>\n <span>Active user count</span>\n </button>\n </div>\n </div>\n </div>\n <div class=\"assistant-chat-area\" data-container=\"chat-area\"></div>\n <div class=\"assistant-input-wrapper\">\n <div class=\"assistant-input-status d-none\" data-ref=\"input-status\"></div>\n <div class=\"assistant-input-box\">\n <textarea class=\"assistant-input\" placeholder=\"Message the assistant...\" rows=\"1\" data-ref=\"input\"></textarea>\n <button class=\"assistant-send-btn\" data-action=\"send\" type=\"button\" title=\"Send message\" data-ref=\"send-btn\">\n <i class=\"bi bi-arrow-up\"></i>\n </button>\n <button class=\"assistant-stop-btn d-none\" data-action=\"stop\" type=\"button\" title=\"Stop generating\" data-ref=\"stop-btn\">\n <i class=\"bi bi-stop-fill\"></i>\n </button>\n </div>\n <div class=\"assistant-input-footer\">\n <span class=\"assistant-connection-indicator\" data-ref=\"status\">\n <span class=\"status-dot connected\"></span>\n </span>\n <span class=\"text-muted\">Enter to send</span>\n </div>\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n // Conversation list for history panel\n this.conversations = new AssistantConversationList();\n this.conversations.params.user = this.app?.activeUser?.id;\n this.conversationListView = new AssistantConversationListView({\n containerId: 'conversation-list',\n collection: this.conversations\n });\n this.addChild(this.conversationListView);\n\n // Chat view\n this.chatView = new ChatView({\n containerId: 'chat-area',\n theme: 'compact',\n messageViewClass: AssistantMessageView,\n currentUserId: this.app?.activeUser?.id,\n showFileInput: false,\n showInput: false,\n adapter: this._createAdapter()\n });\n this.addChild(this.chatView);\n\n // Safety net: re-enable input on assistant message\n const origAddMessage = this.chatView.addMessage.bind(this.chatView);\n this.chatView.addMessage = (msg, scroll) => {\n origAddMessage(msg, scroll);\n if (msg.role === 'assistant' && (msg.content || msg.blocks?.length)) {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n }\n };\n\n // Wire conversation list events\n this.conversationListView.on('conversation:select', (data) => {\n this._onConversationSelect(data);\n this._toggleHistory(false);\n });\n this.conversationListView.on('conversation:new', () => {\n this._onNewConversation();\n this._toggleHistory(false);\n });\n this.conversationListView.on('conversation:deleted', (data) => this._onConversationDeleted(data));\n\n this._subscribeWS();\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (textarea) {\n textarea.addEventListener('input', () => this._autoResize(textarea));\n textarea.addEventListener('keydown', (e) => this._handleKeydown(e));\n setTimeout(() => textarea.focus(), 100);\n }\n\n // Load existing conversation after DOM is ready\n if (this.conversationId) {\n this._showChatArea();\n await this.chatView.refresh();\n }\n\n this._updateConnectionStatus();\n this._updateTitle();\n this._setupResizeHandle();\n }\n\n // ── Resize Handle ────────────────────────────────────────\n\n _setupResizeHandle() {\n const handle = this.element?.querySelector('[data-ref=\"resize-handle\"]');\n if (!handle) return;\n\n const MIN_WIDTH = 300;\n const MAX_WIDTH = 700;\n const STORAGE_KEY = 'mojo:assistant_panel_width';\n\n // Restore saved width\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n const w = parseInt(saved, 10);\n if (w >= MIN_WIDTH && w <= MAX_WIDTH) {\n const panelEl = document.getElementById('assistant-panel');\n if (panelEl) panelEl.style.width = w + 'px';\n }\n }\n\n let startX, startWidth;\n\n const onMouseMove = (e) => {\n const delta = startX - e.clientX;\n const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta));\n const panelEl = document.getElementById('assistant-panel');\n if (panelEl) panelEl.style.width = newWidth + 'px';\n };\n\n const onMouseUp = () => {\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n\n // Save width\n const panelEl = document.getElementById('assistant-panel');\n if (panelEl) {\n localStorage.setItem(STORAGE_KEY, parseInt(panelEl.style.width, 10));\n }\n };\n\n handle.addEventListener('mousedown', (e) => {\n e.preventDefault();\n startX = e.clientX;\n const panelEl = document.getElementById('assistant-panel');\n startWidth = panelEl ? panelEl.offsetWidth : 500;\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n document.addEventListener('mousemove', onMouseMove);\n document.addEventListener('mouseup', onMouseUp);\n });\n }\n\n // ── Actions ──────────────────────────────────────────────\n\n onActionToggleHistory() {\n this._toggleHistory(!this._showingHistory);\n }\n\n onActionNewConversation() {\n this._onNewConversation();\n if (this._showingHistory) this._toggleHistory(false);\n }\n\n onActionClosePanel() {\n this.emit('panel:close');\n }\n\n onActionFullscreen() {\n this.emit('panel:fullscreen', { conversationId: this.conversationId });\n }\n\n onActionPopOut() {\n this.emit('panel:popout', { conversationId: this.conversationId });\n }\n\n onActionUseSuggestion(_event, element) {\n const text = element.dataset.text || element.closest('[data-text]')?.dataset.text;\n if (!text) return;\n\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (textarea) {\n textarea.value = text;\n this._autoResize(textarea);\n }\n this._sendMessage();\n }\n\n onActionSend() {\n this._sendMessage();\n }\n\n onActionStop() {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._showSystemMessage('Response cancelled.');\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n // ── History Toggle ───────────────────────────────────────\n\n _toggleHistory(show) {\n this._showingHistory = show;\n const history = this.element?.querySelector('[data-ref=\"history\"]');\n const chat = this.element?.querySelector('[data-ref=\"chat-wrapper\"]');\n const toggleBtn = this.element?.querySelector('[data-action=\"toggle-history\"] i');\n\n if (history) history.classList.toggle('d-none', !show);\n if (chat) chat.classList.toggle('d-none', show);\n if (toggleBtn) toggleBtn.className = show ? 'bi bi-chat-dots' : 'bi bi-list';\n\n if (show) {\n this.conversationListView.refresh();\n }\n }\n\n // ── Input Handling ───────────────────────────────────────\n\n _autoResize(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';\n }\n\n _handleKeydown(e) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n this._sendMessage();\n }\n }\n\n async _sendMessage() {\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (!textarea) return;\n\n const text = textarea.value.trim();\n if (!text) return;\n\n textarea.value = '';\n textarea.style.height = 'auto';\n this._showChatArea();\n await this.chatView.adapter.addNote({ text, files: [] });\n }\n\n _showChatArea() {\n if (this._hasMessages) return;\n this._hasMessages = true;\n\n const welcome = this.element.querySelector('[data-ref=\"welcome\"]');\n const chatArea = this.element.querySelector('[data-container=\"chat-area\"]');\n if (welcome) welcome.classList.add('d-none');\n if (chatArea) chatArea.classList.remove('d-none');\n }\n\n _showWelcome() {\n this._hasMessages = false;\n\n const welcome = this.element.querySelector('[data-ref=\"welcome\"]');\n const chatArea = this.element.querySelector('[data-container=\"chat-area\"]');\n if (welcome) welcome.classList.remove('d-none');\n if (chatArea) chatArea.classList.add('d-none');\n }\n\n _setInputEnabled(enabled, reason) {\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n const sendBtn = this.element?.querySelector('[data-ref=\"send-btn\"]');\n const stopBtn = this.element?.querySelector('[data-ref=\"stop-btn\"]');\n\n if (textarea) textarea.disabled = !enabled;\n if (sendBtn) sendBtn.classList.toggle('d-none', !enabled);\n if (stopBtn) stopBtn.classList.toggle('d-none', enabled);\n\n this._setInputStatus(enabled ? null : reason);\n\n if (this._responseTimeout) clearTimeout(this._responseTimeout);\n if (!enabled) {\n this._responseTimeout = setTimeout(() => this._onResponseTimeout(), 60000);\n } else {\n this._requestStartTime = null;\n }\n }\n\n _setInputStatus(message) {\n const el = this.element?.querySelector('[data-ref=\"input-status\"]');\n if (!el) return;\n if (message) {\n el.innerHTML = `${this._escapeHtml(message)} <span class=\"assistant-input-status-dismiss\">Click to dismiss</span>`;\n el.classList.remove('d-none');\n if (!el._hasDismiss) {\n el._hasDismiss = true;\n el.addEventListener('click', () => {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n });\n }\n } else {\n el.classList.add('d-none');\n el.innerHTML = '';\n }\n }\n\n _onResponseTimeout() {\n this._responseTimeout = null;\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._showSystemMessage('Request timed out. Please try again.');\n }\n\n // ── Title ────────────────────────────────────────────────\n\n _updateTitle(title) {\n const el = this.element?.querySelector('[data-ref=\"panel-title\"]');\n if (!el) return;\n el.textContent = title || (this.conversationId ? 'Assistant' : 'New conversation');\n }\n\n // ── Chat Adapter ─────────────────────────────────────────\n\n _createAdapter() {\n return {\n fetch: async () => {\n if (!this.conversationId) return [];\n try {\n const conversation = new AssistantConversation({ id: this.conversationId });\n await conversation.fetch({ graph: 'detail' });\n const title = conversation.get('title') || conversation.get('summary');\n if (title) this._updateTitle(title);\n const messages = conversation.get('messages') || [];\n const transformed = messages.map(msg => this._transformMessage(msg)).filter(Boolean);\n return AssistantView._collapseMessages(transformed);\n } catch (_err) {\n if (_err.status === 404) {\n this._onNewConversation();\n this._showSystemMessage('Conversation not found.');\n }\n return [];\n }\n },\n addNote: async (data) => {\n if (!data.text || !data.text.trim()) return { success: false };\n\n const userMsg = {\n id: `local-${++this._messageIdCounter}`,\n role: 'user',\n author: {\n id: this.app?.activeUser?.id,\n name: this.app?.activeUser?.get('display_name') || 'You'\n },\n content: data.text,\n timestamp: new Date().toISOString()\n };\n this.chatView.addMessage(userMsg);\n\n this.chatView.showThinking('Thinking...');\n this._requestStartTime = Date.now();\n this._setInputEnabled(false, 'Waiting for response…');\n\n if (this.ws && this.ws.isConnected) {\n this.ws.send({\n type: 'assistant_message',\n message: data.text,\n conversation_id: this.conversationId\n });\n } else {\n try {\n const resp = await this.app.rest.post('/api/assistant', {\n message: data.text,\n conversation_id: this.conversationId\n });\n const respData = resp?.data?.data || resp?.data || resp;\n if (respData.conversation_id) {\n this.conversationId = respData.conversation_id;\n this.app._assistantConversationId = this.conversationId;\n }\n if (respData.response) {\n this.chatView.addMessage(this._transformMessage(respData.response));\n }\n this._setInputEnabled(true);\n } catch (_err) {\n this._handleAPIError(_err);\n }\n }\n\n return { success: true };\n }\n };\n }\n\n // ── WebSocket Subscriptions ───────────────────────────────\n\n _subscribeWS() {\n if (!this.ws) return;\n\n this._wsHandlers = {\n thinking: (data) => this._onThinking(data),\n text: (data) => this._onText(data),\n tool_call: (data) => this._onToolCall(data),\n response: (data) => this._onResponse(data),\n error: (data) => this._onError(data),\n plan: (data) => this._onPlan(data),\n plan_update: (data) => this._onPlanUpdate(data),\n message: (envelope) => this._dispatchWSMessage(envelope),\n connected: () => this._updateConnectionStatus(),\n disconnected: () => this._updateConnectionStatus(),\n reconnecting: () => this._updateConnectionStatus()\n };\n\n this.ws.on('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.on('message:assistant_text', this._wsHandlers.text);\n this.ws.on('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.on('message:assistant_response', this._wsHandlers.response);\n this.ws.on('message:assistant_error', this._wsHandlers.error);\n this.ws.on('message:assistant_plan', this._wsHandlers.plan);\n this.ws.on('message:assistant_plan_update', this._wsHandlers.plan_update);\n this.ws.on('message:message', this._wsHandlers.message);\n this.ws.on('connected', this._wsHandlers.connected);\n this.ws.on('disconnected', this._wsHandlers.disconnected);\n this.ws.on('reconnecting', this._wsHandlers.reconnecting);\n }\n\n _unsubscribeWS() {\n if (!this.ws || !this._wsHandlers) return;\n\n this.ws.off('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.off('message:assistant_text', this._wsHandlers.text);\n this.ws.off('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.off('message:assistant_response', this._wsHandlers.response);\n this.ws.off('message:assistant_error', this._wsHandlers.error);\n this.ws.off('message:assistant_plan', this._wsHandlers.plan);\n this.ws.off('message:assistant_plan_update', this._wsHandlers.plan_update);\n this.ws.off('message:message', this._wsHandlers.message);\n this.ws.off('connected', this._wsHandlers.connected);\n this.ws.off('disconnected', this._wsHandlers.disconnected);\n this.ws.off('reconnecting', this._wsHandlers.reconnecting);\n\n this._wsHandlers = {};\n }\n\n // ── WS Event Handlers ────────────────────────────────────\n\n _dispatchWSMessage(envelope) {\n const inner = envelope?.data;\n if (!inner?.type) return;\n\n switch (inner.type) {\n case 'assistant_thinking': this._onThinking(inner); break;\n case 'assistant_text': this._onText(inner); break;\n case 'assistant_tool_call': this._onToolCall(inner); break;\n case 'assistant_response': this._onResponse(inner); break;\n case 'assistant_error': this._onError(inner); break;\n case 'assistant_plan': this._onPlan(inner); break;\n case 'assistant_plan_update': this._onPlanUpdate(inner); break;\n }\n }\n\n _isMyConversation(data) {\n if (!data.conversation_id) return true;\n if (!this.conversationId) return true;\n return String(data.conversation_id) === String(this.conversationId);\n }\n\n _adoptConversationId(data) {\n if (data.conversation_id && !this.conversationId) {\n this.conversationId = data.conversation_id;\n this.app._assistantConversationId = this.conversationId;\n }\n }\n\n _onThinking(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n this._showChatArea();\n this.chatView.showThinking('Thinking...');\n this._setInputEnabled(false, 'Assistant is thinking…');\n }\n\n /**\n * Intermediate prose alongside tool calls in the same turn. Renders as\n * an assistant bubble, but does NOT clear the thinking indicator or\n * re-enable input — `assistant_response` remains the terminal signal.\n * @private\n */\n _onText(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n // Server is still working — refresh the safety timeout.\n this._resetResponseTimeout();\n\n const msg = this._transformMessage({\n id: data.message_id || `text-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.text || '',\n blocks: data.blocks || [],\n tool_calls: [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n if (msg && (msg.content || msg.blocks?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onToolCall(data) {\n if (!this._isMyConversation(data)) return;\n this.chatView.showThinking(`Using ${data.tool || data.name || 'tool'}...`);\n this._resetResponseTimeout();\n }\n\n _resetResponseTimeout() {\n if (this._responseTimeout) {\n if (this._requestStartTime && (Date.now() - this._requestStartTime) >= 300000) {\n this._onResponseTimeout();\n return;\n }\n clearTimeout(this._responseTimeout);\n this._responseTimeout = setTimeout(() => this._onResponseTimeout(), 60000);\n }\n }\n\n _onResponse(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._adoptConversationId(data);\n\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n\n const msg = this._transformMessage({\n id: data.message_id || `resp-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.response || data.content || data.message || '',\n blocks: data.blocks || [],\n tool_calls: data.tool_calls_made || data.tool_calls || [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n if (msg && (msg.content || msg.blocks?.length || msg.tool_calls?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onError(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._adoptConversationId(data);\n\n const errorText = data.error || data.message || 'An error occurred';\n this._showSystemMessage(errorText);\n }\n\n _onPlan(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n this._showChatArea();\n\n const plan = data.plan;\n if (!plan) return;\n\n this._activePlans[plan.plan_id] = plan;\n\n this.chatView.addMessage({\n id: `plan-${plan.plan_id}`,\n role: 'assistant',\n author: { name: 'Assistant' },\n content: '',\n timestamp: new Date().toISOString(),\n blocks: [{ type: 'progress', ...plan }],\n tool_calls: []\n });\n }\n\n _onPlanUpdate(data) {\n if (!this._isMyConversation(data)) return;\n\n const plan = this._activePlans[data.plan_id];\n if (plan) {\n const step = plan.steps.find(s => s.id === data.step_id);\n if (step) {\n step.status = data.status;\n step.summary = data.summary;\n }\n }\n\n const msgView = this.chatView.messageViews.get(`plan-${data.plan_id}`);\n if (msgView?.updateProgressStep) {\n msgView.updateProgressStep(data.plan_id, data.step_id, data.status, data.summary);\n }\n this._resetResponseTimeout();\n }\n\n // ── Conversation Management ──────────────────────────────\n\n async _onConversationSelect(data) {\n this.conversationId = data.id;\n this.app._assistantConversationId = this.conversationId;\n this.conversationListView.setActive(data.id);\n this._showChatArea();\n this._updateTitle(data.model?.get('title') || data.model?.get('summary'));\n await this.chatView.refresh();\n }\n\n _onNewConversation() {\n this.conversationId = null;\n this.app._assistantConversationId = null;\n this.conversationListView.setActive(null);\n this.chatView.clearMessages();\n this._setInputEnabled(true);\n this._showWelcome();\n this._updateTitle();\n\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n _onConversationDeleted(data) {\n if (String(data.id) === String(this.conversationId)) {\n this._onNewConversation();\n }\n }\n\n // ── Helpers ──────────────────────────────────────────────\n\n _transformMessage(msg) {\n if (msg.role === 'tool_result') return null;\n\n let content = msg.content || msg.text || '';\n let blocks = msg.blocks || [];\n let toolCalls = msg.tool_calls || [];\n\n if (toolCalls.length > 0) {\n // Normalize WS `tool_calls_made` shape `{ tool, input }` → Anthropic `{ type, name, input }`\n toolCalls = toolCalls.map(tc =>\n (!tc.type && tc.tool)\n ? { type: 'tool_use', name: tc.tool, input: tc.input }\n : tc\n );\n const textParts = toolCalls\n .filter(tc => tc.type === 'text' && tc.text)\n .map(tc => tc.text);\n if (!content && textParts.length > 0) {\n content = textParts.join('\\n\\n');\n }\n toolCalls = toolCalls\n .filter(tc => tc.type === 'tool_use')\n .filter(tc => !AssistantView.INTERNAL_TOOLS.has(tc.name));\n }\n\n if (blocks.length === 0 && content.includes('assistant_block')) {\n const parsed = AssistantView._parseBlocks(content);\n content = parsed.content;\n blocks = parsed.blocks;\n }\n\n const currentUserId = this.app?.activeUser?.id;\n\n return {\n id: msg.id,\n role: msg.role || 'user',\n author: msg.role === 'assistant'\n ? { name: 'Assistant' }\n : msg.author || {\n name: msg.user?.display_name || this.app?.activeUser?.get('display_name') || 'You',\n id: msg.user?.id || currentUserId\n },\n content,\n timestamp: msg.created || msg.timestamp,\n blocks,\n tool_calls: toolCalls,\n _conversationId: this.conversationId\n };\n }\n\n _showSystemMessage(text) {\n this._showChatArea();\n this.chatView.addMessage({\n id: `sys-${++this._messageIdCounter}`,\n type: 'system_event',\n content: text,\n timestamp: new Date().toISOString()\n });\n }\n\n _handleAPIError(_err) {\n if (_err.status === 404) {\n this._showSystemMessage('Assistant is not enabled on this server.');\n } else if (_err.status === 503) {\n this._showSystemMessage('LLM API key not configured. Contact your administrator.');\n } else {\n this._showSystemMessage('Failed to send message. Please try again.');\n }\n this._setInputEnabled(true);\n }\n\n _updateConnectionStatus() {\n const dot = this.element?.querySelector('.status-dot');\n if (!dot) return;\n\n if (this.ws?.isConnected) {\n dot.className = 'status-dot connected';\n dot.title = 'Connected';\n if (!this._responseTimeout) {\n this._setInputEnabled(true);\n } else {\n this._setInputEnabled(false, 'Waiting for response…');\n }\n } else if (this.ws?.isReconnecting) {\n dot.className = 'status-dot reconnecting';\n dot.title = 'Reconnecting...';\n this._setInputEnabled(false, 'Reconnecting…');\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n } else {\n dot.className = 'status-dot disconnected';\n dot.title = 'Disconnected';\n this._setInputEnabled(false, 'Disconnected — reconnecting…');\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n }\n }\n\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n /**\n * Focus the input textarea — called externally when reopening the panel.\n */\n focusInput() {\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n async onBeforeDestroy() {\n this._unsubscribeWS();\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n }\n}\n\nexport default AssistantPanelView;\n"],"names":["AssistantPanelView","View","constructor","options","super","className","this","app","ws","conversationId","_assistantConversationId","_wsHandlers","_messageIdCounter","_hasMessages","_activePlans","_requestStartTime","_showingHistory","getTemplate","_escapeHtml","activeUser","get","onInit","conversations","AssistantConversationList","params","user","id","conversationListView","AssistantConversationListView","containerId","collection","addChild","chatView","ChatView","theme","messageViewClass","AssistantMessageView","currentUserId","showFileInput","showInput","adapter","_createAdapter","origAddMessage","addMessage","bind","msg","scroll","role","content","blocks","length","hideThinking","_setInputEnabled","on","data","_onConversationSelect","_toggleHistory","_onNewConversation","_onConversationDeleted","_subscribeWS","onAfterRender","textarea","element","querySelector","addEventListener","_autoResize","e","_handleKeydown","setTimeout","focus","_showChatArea","refresh","_updateConnectionStatus","_updateTitle","_setupResizeHandle","handle","STORAGE_KEY","saved","localStorage","getItem","w","parseInt","panelEl","document","getElementById","style","width","startX","startWidth","onMouseMove","delta","clientX","newWidth","Math","min","max","onMouseUp","removeEventListener","body","cursor","userSelect","setItem","preventDefault","offsetWidth","onActionToggleHistory","onActionNewConversation","onActionClosePanel","emit","onActionFullscreen","onActionPopOut","onActionUseSuggestion","_event","text","dataset","closest","value","_sendMessage","onActionSend","onActionStop","_showSystemMessage","show","history","chat","toggleBtn","classList","toggle","height","scrollHeight","key","shiftKey","trim","addNote","files","welcome","chatArea","add","remove","_showWelcome","enabled","reason","sendBtn","stopBtn","disabled","_setInputStatus","_responseTimeout","clearTimeout","_onResponseTimeout","message","el","innerHTML","_hasDismiss","title","textContent","fetch","async","conversation","AssistantConversation","graph","transformed","map","_transformMessage","filter","Boolean","AssistantView","_collapseMessages","_err","status","success","userMsg","author","name","timestamp","Date","toISOString","showThinking","now","isConnected","send","type","conversation_id","resp","rest","post","respData","response","_handleAPIError","thinking","_onThinking","_onText","tool_call","_onToolCall","_onResponse","error","_onError","plan","_onPlan","plan_update","_onPlanUpdate","envelope","_dispatchWSMessage","connected","disconnected","reconnecting","_unsubscribeWS","off","inner","_isMyConversation","String","_adoptConversationId","_resetResponseTimeout","message_id","tool_calls","created","tool","tool_calls_made","errorText","plan_id","step","steps","find","s","step_id","summary","msgView","messageViews","updateProgressStep","setActive","model","clearMessages","toolCalls","tc","input","textParts","join","INTERNAL_TOOLS","has","includes","parsed","_parseBlocks","display_name","_conversationId","dot","isReconnecting","div","createElement","focusInput","onBeforeDestroy"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./Collection-BZlmtcuL.js"),t=require("./ContextMenu-BPPtuqKk.js"),i=require("./Modal-DBJU16cc.js"),s=require("./Passkeys-Dr8-oSm9.js"),n=require("./View-Yazho7OL.js"),a=require("./DataView-BbrwHMV4.js"),o=require("./FormView-Dcy7XOtC.js");class MetricsPermission extends e.Model{constructor(e={}){super(e,{endpoint:"/api/metrics/permissions",id_key:"account"})}}class MetricsPermissionList extends e.Collection{constructor(e={}){super({ModelClass:MetricsPermission,endpoint:"/api/metrics/permissions",...e})}}const l={title:"Edit Location",size:"lg",fields:[{name:"ip_address",label:"IP Address",type:"text",required:!0,readonly:!0,cols:6},{name:"subnet",label:"Subnet",type:"text",cols:6},{name:"country_name",label:"Country",type:"text",cols:6},{name:"country_code",label:"Country Code",type:"text",cols:6},{name:"region",label:"Region",type:"text",cols:6},{name:"city",label:"City",type:"text",cols:6},{name:"postal_code",label:"Postal Code",type:"text",cols:6},{name:"timezone",label:"Timezone",type:"text",cols:6},{name:"latitude",label:"Latitude",type:"number",step:"any",cols:6},{name:"longitude",label:"Longitude",type:"number",step:"any",cols:6}]};class GeoLocatedIP extends e.Model{constructor(e={}){super(e,{endpoint:"/api/system/geoip"})}static async lookup(e){const t=new GeoLocatedIP,i=await t.rest.GET("/api/system/geoip/lookup",{ip:e});return i.success&&i.data&&i.data.data?new GeoLocatedIP(i.data.data):null}}GeoLocatedIP.EDIT_FORM=l,GeoLocatedIP.EDIT_LOCATION_FORM=l,GeoLocatedIP.EDIT_SECURITY_FORM={title:"Edit Security",size:"md",fields:[{name:"threat_level",label:"Threat Level",type:"select",cols:12,options:[{value:"",label:"None"},{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"},{value:"critical",label:"Critical"}]},{name:"is_threat",label:"Threat",type:"switch",cols:6},{name:"is_suspicious",label:"Suspicious",type:"switch",cols:6},{name:"is_known_attacker",label:"Known Attacker",type:"switch",cols:6},{name:"is_known_abuser",label:"Known Abuser",type:"switch",cols:6},{name:"risk_score",label:"Risk Score",type:"number",cols:6},{name:"is_tor",label:"TOR Exit Node",type:"switch",cols:6},{name:"is_vpn",label:"VPN",type:"switch",cols:6},{name:"is_proxy",label:"Proxy",type:"switch",cols:6},{name:"is_cloud",label:"Cloud Provider",type:"switch",cols:6},{name:"is_datacenter",label:"Datacenter",type:"switch",cols:6}]},GeoLocatedIP.EDIT_NETWORK_FORM={title:"Edit Network",size:"md",fields:[{name:"asn",label:"ASN",type:"text",cols:6},{name:"asn_org",label:"ASN Organization",type:"text",cols:6},{name:"isp",label:"ISP",type:"text",cols:12},{name:"connection_type",label:"Connection Type",type:"text",cols:6},{name:"provider",label:"Provider",type:"text",cols:6},{name:"is_mobile",label:"Mobile Connection",type:"switch",cols:6},{name:"mobile_carrier",label:"Mobile Carrier",type:"text",cols:6},{name:"last_seen",label:"Last Seen",type:"datetime",cols:12}]};class GeoLocatedIPList extends e.Collection{constructor(e={}){super({ModelClass:GeoLocatedIP,endpoint:"/api/system/geoip",...e})}}class TablePage extends t.Page{constructor(e={}){super({...e,pageName:e.pageName||e.name||"table"}),this.title=e.title||this.pageName,this.description=e.description||"",this.Collection=e.Collection||null,this.collection=e.collection||null,this.defaultQuery=e.defaultQuery||{},this.groupField=e.groupField||"group",this.tableViewConfig={columns:e.columns||[],actions:e.actions||null,contextMenu:e.contextMenu||null,batchActions:e.batchActions||null,batchBarLocation:e.batchBarLocation||"top",clickAction:e.clickAction||"view",addForm:e.addForm||e.formFields||e.formCreate,editForm:e.editForm||e.formEdit||e.formFields,itemView:e.itemView||e.itemViewClass,deleteTemplate:e.deleteTemplate,formDialogConfig:e.formDialogConfig,viewDialogOptions:e.viewDialogOptions,searchable:!1!==e.searchable,sortable:!1!==e.sortable,filterable:!1!==e.filterable,paginated:!1!==e.paginated,selectionMode:e.selectionMode||(e.selectable?"multiple":"none"),filters:e.filters||e.additionalFilters||[],hideActivePills:e.hideActivePills||!1,hideActivePillNames:e.hideActivePillNames||[],searchPlacement:e.searchPlacement||"toolbar",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1,...e.tableOptions},emptyMessage:e.emptyMessage||"No data available",searchPlaceholder:e.searchPlaceholder||"Search...",showAdd:!1!==e.showAdd,showExport:!1!==e.showExport,onItemView:e.onItemView,onItemEdit:e.onItemEdit,onItemDelete:e.onItemDelete,onAdd:e.onAdd,onExport:e.onExport,...e.tableViewOptions},this.urlSyncEnabled=!1!==e.urlSyncEnabled,this.lastUpdated=null,this.isLoading=!1,this.template=e.template||this.buildTemplate()}buildTemplate(){return'\n <div class="table-page-container">\n\n <div class="table-container" data-container="table"></div>\n\n {{#showStatus}}\n <div class="table-status-bar table-status-top">\n <div class="status-info">\n <div class="d-flex justify-content-between w-100">\n <span class="text-muted">\n <i class="bi bi-clock me-1"></i>\n Last updated: <span data-status="last-updated">{{lastUpdated}}</span>\n </span>\n <span class="text-muted">\n <i class="bi bi-list-ol me-1"></i>\n Total records: <span data-status="record-count">0</span>\n </span>\n </div>\n </div>\n </div>\n {{/showStatus}}\n\n </div>\n '}async onInit(){await super.onInit(),this.collection||(this.Collection?this.collection=new this.Collection:this.collection=new e.Collection),this.applyQueryToCollection(),this.tableView=new s.TableView({collection:this.collection,containerId:"table",fetchOnMount:!0,...this.tableViewConfig,onItemView:async(e,t)=>{if(this.tableViewConfig.onItemView)return this.tableViewConfig.onItemView(e,t);await this.showItemDialog(e)}}),this.addChild(this.tableView),this.setupEventListeners()}setupEventListeners(){this.urlSyncEnabled&&this.collection&&(this.collection.on("fetch:start",()=>{this.isLoading=!0}),this.collection.on("fetch:end",()=>{this.isLoading=!1,this.lastUpdated=/* @__PURE__ */(new Date).toLocaleTimeString(),this.updateStatusDisplay()})),this.tableView.on("params-changed",()=>{this.urlSyncEnabled&&this.syncUrl()}),this.tableView.on("filter:edit",async({key:e})=>{await this.handleFilterEdit(e)}),this.tableView.on("row:view",async({model:e})=>{this.onItemView&&await this.onItemView(e)}),this.tableView.on("row:edit",async({model:e})=>{this.onItemEdit&&await this.onItemEdit(e)}),this.tableView.on("row:delete",async({model:e})=>{this.onItemDelete&&await this.onItemDelete(e)}),this.tableView.on("table:add",async({event:e})=>{}),this.tableView.on("table:export",async({data:e})=>{this.tableViewConfig.onExport&&await this.tableViewConfig.onExport(e)})}applyQueryToCollection(){const e={},t={...this.defaultQuery,...this.query};if(!t||0===Object.keys(t).length)return;void 0!==t.start&&(e.start=parseInt(t.start)||0),void 0!==t.size&&(e.size=parseInt(t.size)||10),void 0!==t.sort&&(e.sort=t.sort),void 0!==t.search&&(e.search=t.search);const i=["start","size","sort","search","page","_item"];Object.entries(t).forEach(([t,s])=>{if(!i.includes(t)&&void 0!==s&&""!==s)if("string"==typeof s&&(s.startsWith("{")||s.startsWith("[")))try{e[t]=JSON.parse(s)}catch(n){e[t]=s}else e[t]=s}),Object.keys(e).length>0&&(Object.keys(e).forEach(t=>{const{field:i,lookup:n}=s.parseFilterKey(t);"in"!==n&&"not_in"!==n||!e.hasOwnProperty(i)||delete e[i]}),this.collection.setParams({...this.collection.params,...e}))}syncUrl(e=!0){if(!this.urlSyncEnabled||!this.collection||!this.getApp()?.router)return;const t=new URL(window.location),i={};for(const[o,l]of t.searchParams)"page"!==o&&(i[o]=l);const s={},n=this.collection.params||{};n.start&&(s.start=n.start),n.size&&(s.size=n.size),n.sort&&(s.sort=n.sort),n.search&&(s.search=n.search),Object.entries(n).forEach(([e,t])=>{["start","size","sort","search"].includes(e)||void 0===t||""===t||(s[e]="object"==typeof t?JSON.stringify(t):t)});const a=Object.keys(s).some(e=>String(i[e]||"")!==String(s[e]||""))||Object.keys(i).some(e=>!(e in s));this.query._item&&(s._item=this.query._item),this.query=s,(a||e)&&this.updateBrowserUrl(s,!0,!1)}updateStatusDisplay(){if(!this.element)return;const e=this.element.querySelector('[data-status="last-updated"]');e&&(e.textContent=this.lastUpdated||"Never");const t=this.element.querySelector('[data-status="record-count"]');if(t&&this.collection){const e=this.collection.meta?.count||this.collection.length();t.textContent=e}}async onEnter(){await super.onEnter(),this.options.requiresGroup&&!this.query[this.groupField]&&this.getApp().activeGroup&&(this.query[this.groupField]=this.getApp().activeGroup.id),this.applyQueryToCollection(),this.tableView&&this.tableView.element&&setTimeout(()=>{this.tableView.updateFilterPills(),this.tableView.updateSortIcons()},100),this.query._item&&this._openDeepLinkedItem(this.query._item)}async _openDeepLinkedItem(e){try{const t=await this.collection.fetchOne(e);t&&await this.showItemDialog(t)}catch(t){this._clearItemParam()}}async showItemDialog(e){this._setItemParam(e.id);const t=this.tableView.getItemViewClass(e);if(t){const s=new t({model:e,collection:this.collection});await i.Modal.dialog({header:!1,body:s,size:"lg",centered:!1,...this.tableView.getFormDialogConfig(this.tableView.getModelClass(e)),...this.tableView.viewDialogOptions})}else await i.Modal.data({title:`View ${this.tableView.getModelName(e)} #${e.id}`,model:e});this._clearItemParam()}_setItemParam(e){const t={...this.query,_item:e};this.query=t,this.updateBrowserUrl(t,!0,!1)}_clearItemParam(){delete this.query._item,this.updateBrowserUrl(this.query,!0,!1)}async refresh(){await this.tableView.refresh()}getSelectedItems(){return this.tableView.getSelectedItems()}clearSelection(){this.tableView.clearSelection()}async handleFilterEdit(e){const t=this.tableView.getAllAvailableFilters().find(t=>t.key===e),s=this.collection.params[e];if(!t)return;const n={name:"filter_value",label:t.label||e,value:s,...t.config},a=await i.Modal.form({title:`Edit ${n.label} Filter`,size:"md",fields:[n]});a&&void 0!==a.filter_value&&(this.tableView.setFilter(e,a.filter_value),this.collection.restEnabled&&this.collection.fetch(),await this.tableView.render(),this.syncUrl())}clearAllFilters(){if(!this.collection)return;const{start:e,size:t,sort:i}=this.collection.params;this.collection.params={start:e,size:t},i&&(this.collection.params.sort=i),this.syncUrl(),this.collection.restEnabled?this.collection.fetch():this.tableView.render()}async onGroupChange(e){e&&this.collection&&this.options.requiresGroup&&(this.query[this.groupField]=e.id,this.applyQueryToCollection(),this.collection&&this.collection.restEnabled&&this.collection.fetch())}async onBeforeDestroy(){this.collection&&(this.collection.off("fetch:start"),this.collection.off("fetch:end")),this.tableView&&(this.tableView.off("params-changed"),this.tableView.off("table:search"),this.tableView.off("table:sort"),this.tableView.off("table:page"),this.tableView.off("filter:edit"),this.tableView.off("row:view"),this.tableView.off("row:edit"),this.tableView.off("row:delete"),this.tableView.off("table:add"),this.tableView.off("table:export")),await super.onBeforeDestroy()}get showStatus(){return!0===this.options.showStatus}static create(e={}){return new this(e)}}class TabView extends n.View{constructor(e={}){const{tabs:t,activeTab:i,tabsClass:s,contentClass:n,minWidth:a,enableResponsive:o,tabPadding:l,dropdownStyle:r,enableTransitions:c,transitionDuration:d,...h}=e;super({tagName:"div",className:"tab-view",...h}),this.tabs={},this.tabLabels=Object.keys(this.tabs),this.activeTab=i||this.tabLabels[0]||null,this.tabsClass=s||"nav nav-tabs mb-3",this.contentClass=n||"tab-content",this.enableTransitions=!1!==c,this.transitionDuration=d||150,this.dropdownStyle=r||"select",this.minWidth=a||300,this.enableResponsive=!1!==o,this.tabPadding=l||80,this.currentMode="tabs",this.tabWidthCache=/* @__PURE__ */new Map,this.lastContainerWidth=0,this.resizeObserver=null,this._measurementSpan=null,this._tabComputedStyle=null,this.isMobileMode=!1,this.hasOverflow=!1;for(const[m,p]of Object.entries(t))this.addTab(m,p);this.handleResize=this.handleResize.bind(this)}async renderTemplate(){return`\n <div class="tab-view-container">\n ${this.buildTabNavigation()}\n ${this.buildTabContent()}\n </div>\n `}buildTabNavigation(){return 0===this.tabLabels.length?"":"dropdown"===this.currentMode?this.buildDropdownNavigation():this.buildTabsNavigation()}buildTabsNavigation(){const e=this.tabLabels.map(e=>{const t=e===this.activeTab,i=this.getTabId(e);return`\n <li class="nav-item" role="presentation">\n <button class="nav-link ${t?"active":""}"\n id="${i}-tab"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button"\n role="tab"\n aria-controls="${i}"\n aria-selected="${t}">\n ${this.escapeHtml(e)}\n </button>\n </li>\n `}).join("");return`\n <ul class="${this.tabsClass}" role="tablist" data-tab-mode="tabs">\n ${e}\n </ul>\n `}buildDropdownNavigation(){const e=this.activeTab||this.tabLabels[0],t=this.tabLabels.map(e=>{const t=e===this.activeTab;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button">\n ${this.escapeHtml(e)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");let i;return i="select"===this.dropdownStyle?`\n <button class="btn tab-view-select-style dropdown-toggle"\n type="button"\n data-bs-toggle="dropdown"\n aria-expanded="false"\n id="tab-dropdown-${this.id}">\n <span class="tab-view-select-label">${this.escapeHtml(e)}</span>\n </button>\n `:`\n <button class="btn btn-outline-secondary dropdown-toggle w-100 w-sm-auto"\n type="button"\n data-bs-toggle="dropdown"\n aria-expanded="false"\n id="tab-dropdown-${this.id}">\n <i class="bi bi-list me-2"></i>\n ${this.escapeHtml(e)}\n </button>\n `,`\n <div class="dropdown mb-3" data-tab-mode="dropdown">\n ${i}\n <ul class="dropdown-menu" aria-labelledby="tab-dropdown-${this.id}">\n ${t}\n </ul>\n </div>\n `}buildMobileDropdownNavigation(){const e=this.activeTab||this.tabLabels[0],t=this.tabLabels.map(e=>{const t=e===this.activeTab;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button">\n ${this.escapeHtml(e)}\n ${t?'<i class="bi bi-check ms-2"></i>':""}\n </button>\n </li>\n `}).join("");return`\n <div class="dropdown mb-3" data-tab-navigation="mobile">\n <button class="btn btn-outline-secondary dropdown-toggle w-100 text-start"\n type="button"\n data-bs-toggle="dropdown"\n aria-expanded="false">\n <i class="bi bi-list me-2"></i>\n ${this.escapeHtml(e)}\n </button>\n <ul class="dropdown-menu w-100">\n ${t}\n </ul>\n </div>\n `}shouldUseMobileDropdown(){if(!this.enableMobileDropdown)return!1;const e=window.innerWidth;return e<this.mobileBreakpoint||this.hasOverflow&&e<992}buildTabContent(){if(0===this.tabLabels.length)return'<div class="alert alert-info">No tabs to display</div>';const e=this.tabLabels.map(e=>{const t=e===this.activeTab,i=this.getTabId(e);return`\n <div class="tab-pane fade ${t?"show active":""}"\n id="${i}"\n role="tabpanel"\n aria-labelledby="${i}-tab"\n data-tab-label="${this.escapeHtml(e)}">\n <div data-container="${i}-content"></div>\n </div>\n `}).join("");return`\n <div class="${this.contentClass}">\n ${e}\n </div>\n `}getTabId(e){return`tab-${e.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${this.id}`}async showTab(e,t={}){const{force:i=!1}=t;if(!this.tabs[e])return console.warn(`TabView: Tab "${e}" does not exist`),!1;if(this.activeTab===e&&!i){const t=this.tabs[e];if(t&&t.isMounted()&&this.element.contains(t.element))return!0}const s=this.activeTab;this.activeTab=e;try{return await this.updateTabNavigation(e,s),await this.updateTabContent(e,s),this.emit("tab:changed",{activeTab:e,previousTab:s}),!0}catch(n){return console.error("TabView: Error showing tab:",n),this.activeTab=s,!1}}async updateTabNavigation(e,t){if(!this.element)return;if("dropdown"===this.currentMode)return void(await this.reRenderNavigation());if(t){const e=this.element.querySelector(`[data-tab-label="${t}"]`);e&&(e.classList.remove("active"),e.setAttribute("aria-selected","false"))}const i=this.element.querySelector(`[data-tab-label="${e}"]`);i&&(i.classList.add("active"),i.setAttribute("aria-selected","true"))}async updateTabContent(e,t){if(!this.element)return;const i=this.getTabId(e),s=t?this.getTabId(t):null,n=this.element.querySelector(`#${i}`),a=s?this.element.querySelector(`#${s}`):null,o=this.tabs[e];if(this.enableTransitions){if(a&&a.classList.contains("show")&&(a.classList.remove("show"),await new Promise(e=>{const t=this._getTransitionDuration(a)||this.transitionDuration;setTimeout(e,t)}),a.classList.remove("active")),o){const e=this.element.querySelector(`[data-container="${i}-content"]`);e&&!o.isMounted()&&await o.render(!0,e)}n&&(n.classList.add("active"),n.offsetHeight,n.classList.add("show"))}else{if(a&&a.classList.remove("show","active"),o){const e=this.element.querySelector(`[data-container="${i}-content"]`);e&&!o.isMounted()&&await o.render(!0,e)}n&&n.classList.add("show","active")}o&&o.onTabActivated&&await o.onTabActivated()}_getTransitionDuration(e){if(!e||"undefined"==typeof window||!window.getComputedStyle)return 0;const t=(window.getComputedStyle(e).transitionDuration||"0s").match(/^([0-9.]+)(m?s)$/);if(!t)return 0;const i=parseFloat(t[1]);return"s"===t[2]?1e3*i:i}async onActionShowTab(e,t){const i=t.getAttribute("data-tab-label");i&&await this.showTab(i)}_initializeMeasurementStyles(){if(!this.element||this._tabComputedStyle)return;const e=this.element.querySelector(".nav-link");if(e&&"function"==typeof window.getComputedStyle){const t=window.getComputedStyle(e);this._tabComputedStyle={font:t.font,letterSpacing:t.letterSpacing};const i=parseFloat(t.paddingLeft)||0,s=parseFloat(t.paddingRight)||0;this.tabPadding=i+s+12}}async onAfterRender(){await super.onAfterRender(),this._initializeMeasurementStyles(),this.enableResponsive&&this.setupResponsiveHandling(),this.activeTab&&this.tabs[this.activeTab]&&await this.showTab(this.activeTab,{force:!0})}async onAfterMount(){await super.onAfterMount()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize),this._measurementSpan&&this._measurementSpan.parentElement&&this._measurementSpan.parentElement.removeChild(this._measurementSpan),this._measurementSpan=null;for(const[e,t]of Object.entries(this.tabs))t&&"function"==typeof t.destroy&&await t.destroy()}async onSectionActivated(){const e=this.activeTab?this.tabs[this.activeTab]:null;e?.onTabActivated&&await e.onTabActivated()}getActiveTab(){return this.activeTab}getTabLabels(){return[...this.tabLabels]}getTab(e){return this.tabs[e]||null}async addTab(e,t,i=!1){return this.tabs[e]?(console.warn(`TabView: Tab "${e}" already exists`),!1):!(t.options.permissions&&!this.getApp().activeUser.hasPerm(t.options.permissions)||(this.tabs[e]=t,t.containerId=this.getTabId(e),t.parent=this,this.tabLabels=Object.keys(this.tabs),this.activeTab&&!i||(this.activeTab=e),this.isMounted()&&(await this.render(),(i||this.activeTab===e)&&await this.showTab(e)),this.emit("tab:added",{label:e,view:t}),0))}async removeTab(e){if(!this.tabs[e])return console.warn(`TabView: Tab "${e}" does not exist`),!1;const t=this.tabs[e];return t&&"function"==typeof t.destroy&&await t.destroy(),delete this.tabs[e],this.tabLabels=Object.keys(this.tabs),this.activeTab===e&&(this.activeTab=this.tabLabels[0]||null),this.isMounted()&&(await this.render(),this.activeTab&&await this.showTab(this.activeTab)),this.emit("tab:removed",{label:e,view:t}),!0}calculateTabWidth(e){if(this.tabWidthCache.has(e))return this.tabWidthCache.get(e);if("undefined"==typeof document){const t=8*e.length+this.tabPadding;return this.tabWidthCache.set(e,t),t}this._measurementSpan||(this._measurementSpan=document.createElement("span"),this._measurementSpan.style.visibility="hidden",this._measurementSpan.style.position="absolute",this._measurementSpan.style.whiteSpace="nowrap");const t=this._measurementSpan;this._tabComputedStyle?(t.style.font=this._tabComputedStyle.font,t.style.letterSpacing=this._tabComputedStyle.letterSpacing):(t.style.fontSize="14px",t.style.fontFamily="system-ui, -apple-system, sans-serif"),t.textContent=e,document.body.appendChild(t);const i=t.offsetWidth+this.tabPadding;return document.body.removeChild(t),this.tabWidthCache.set(e,i),i}getTotalTabWidth(){return this.tabLabels.reduce((e,t)=>e+this.calculateTabWidth(t),0)}getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}shouldUseDropdown(){if(!this.enableResponsive)return!1;const e=this.getContainerWidth(),t=this.getTotalTabWidth();return e<Math.max(t,this.minWidth)}setupResponsiveHandling(){if(this.element&&this.enableResponsive)if(this.updateNavigationMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this.getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this.updateNavigationMode())}async updateNavigationMode(){const e=this.shouldUseDropdown()?"dropdown":"tabs";e!==this.currentMode&&(this.currentMode=e,this.isMounted()&&await this.reRenderNavigation(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:this.getContainerWidth(),totalTabWidth:this.getTotalTabWidth()}))}async reRenderNavigation(){if(!this.element)return;const e=this.element.querySelector("[data-tab-mode]");if(e){const t=this.buildTabNavigation();e.outerHTML=t}}getNavigationMode(){return this.currentMode}async setNavigationMode(e){"tabs"===e||"dropdown"===e?(this.currentMode=e,this.isMounted()&&await this.reRenderNavigation()):console.warn('TabView: Invalid navigation mode. Use "tabs" or "dropdown"')}clearWidthCache(){this.tabWidthCache.clear()}static create(e={}){return new TabView(e)}}class SideNavView extends n.View{constructor(e={}){const{sections:t=[],activeSection:i,navWidth:s,contentPadding:n,enableResponsive:a,minWidth:o,...l}=e;super({tagName:"div",className:"side-nav-view",...l}),this.navWidth=s||200,this.contentPadding=n||"1.5rem 2.5rem",this.enableResponsive=!1!==a,this.minWidth=o||500,this.sectionConfigs=[],this.sectionViews={},this.sectionKeys=[],this.activeSection=null,this.currentMode="sidebar",this.resizeObserver=null,this.lastContainerWidth=0;for(const r of t)this._addSectionConfig(r);this.activeSection=i||this.sectionKeys[0]||null,this.handleResize=this.handleResize.bind(this)}_addSectionConfig(e){"divider"!==e.type?e.permissions&&!this._hasPermission(e.permissions)||(this.sectionConfigs.push(e),this.sectionKeys.push(e.key),e.view&&(this.sectionViews[e.key]=e.view,e.view.parent=this)):this.sectionConfigs.push({type:"divider",label:e.label})}_hasPermission(e){try{return this.getApp().activeUser.hasPerm(e)}catch{return!0}}async renderTemplate(){const e="dropdown"===this.currentMode?this._buildDropdownNav():this._buildSidebarNav();return`\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${"dropdown"===this.currentMode?`\n <div class="snv-dropdown">${e}</div>\n <div class="snv-content" data-container="snv-content"></div>\n `:`\n <div class="snv-layout">\n <nav class="snv-nav">${e}</nav>\n <div class="snv-content" data-container="snv-content"></div>\n </div>\n `}\n `}_buildSidebarNav(){return this.sectionConfigs.map(e=>{if("divider"===e.type)return`<div class="snv-nav-label">${this.escapeHtml(e.label)}</div>`;const t=e.key===this.activeSection,i=e.icon?`<i class="bi ${e.icon}"></i>`:"";return`<a role="button" class="${t?"active":""}" data-action="navigate" data-section="${e.key}">${i} ${this.escapeHtml(e.label)}</a>`}).join("")}_buildDropdownNav(){const e=this.sectionConfigs.find(e=>e.key===this.activeSection),t=e?e.label:this.sectionKeys[0],i=this.sectionConfigs.filter(e=>"divider"!==e.type).map(e=>{const t=e.key===this.activeSection;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="navigate"\n data-section="${e.key}"\n type="button">\n ${e.icon?`<i class="bi ${e.icon} me-2"></i>`:""}\n ${this.escapeHtml(e.label)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");return`\n <div class="dropdown">\n <button class="snv-select-btn" type="button"\n data-bs-toggle="dropdown" aria-expanded="false">\n ${e?.icon?`<i class="bi ${e.icon}"></i>`:""}\n <span>${this.escapeHtml(t)}</span>\n </button>\n <ul class="dropdown-menu w-100">${i}</ul>\n </div>\n `}async onAfterRender(){await super.onAfterRender(),this.activeSection&&await this._mountSection(this.activeSection),this.enableResponsive&&this._setupResponsive()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize);for(const e of Object.values(this.sectionViews))e&&"function"==typeof e.destroy&&await e.destroy()}async showSection(e){if(!this.sectionViews[e])return console.warn(`SideNavView: Section "${e}" does not exist`),!1;if(e===this.activeSection){const t=this.sectionViews[e];if(t&&t.isMounted()&&this.element?.contains(t.element))return!0}const t=this.activeSection;this.activeSection=e,t&&t!==e&&await this._unmountSection(t),await this._mountSection(e);const i=this.sectionViews[e];return i?.onSectionActivated&&await i.onSectionActivated(),this._updateNavState(e),this.emit("section:changed",{activeSection:e,previousSection:t}),!0}async _mountSection(e){const t=this.sectionViews[e];if(!t)return;const i=this.element?.querySelector('[data-container="snv-content"]');if(i&&!t.isMounted()){this._showContentLoading(i);try{await t.render(!0,i)}finally{this._hideContentLoading(i)}}}_showContentLoading(e){if(!e)return;let t=e.querySelector(".snv-loading");t||(t=document.createElement("div"),t.className="snv-loading",t.innerHTML='<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>',t.style.cssText="display:flex;align-items:center;justify-content:center;padding:3rem;",e.prepend(t))}_hideContentLoading(e){if(!e)return;const t=e.querySelector(".snv-loading");t&&t.remove()}async _unmountSection(e){const t=this.sectionViews[e];t&&t.isMounted()&&await t.unmount()}_updateNavState(e){if(!this.element)return;this.element.querySelectorAll(".snv-nav a, .dropdown-item").forEach(t=>{const i=t.dataset.section;i&&t.classList.toggle("active",i===e)});const t=this.element.querySelector(".snv-select-btn span");if(t){const i=this.sectionConfigs.find(t=>t.key===e);i&&(t.textContent=i.label)}}async onActionNavigate(e,t){e.preventDefault();const i=t.dataset.section;return i&&await this.showSection(i),!0}_setupResponsive(){if(this.element&&this.enableResponsive)if(this._updateMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this._getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this._updateMode())}_getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}async _updateMode(){const e=this._getContainerWidth(),t=e<this.minWidth?"dropdown":"sidebar";t!==this.currentMode&&(this.currentMode=t,this.isMounted()&&await this.render(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:e}))}getActiveSection(){return this.activeSection}getSectionKeys(){return[...this.sectionKeys]}getSection(e){return this.sectionViews[e]||null}async addSection(e,t=!1){return e.key&&this.sectionViews[e.key]?(console.warn(`SideNavView: Section "${e.key}" already exists`),!1):(this._addSectionConfig(e),this.isMounted()&&(await this.render(),t&&e.key&&await this.showSection(e.key)),this.emit("section:added",{config:e}),!0)}async removeSection(e){const t=this.sectionViews[e];return t?("function"==typeof t.destroy&&await t.destroy(),delete this.sectionViews[e],this.sectionKeys=this.sectionKeys.filter(t=>t!==e),this.sectionConfigs=this.sectionConfigs.filter(t=>t.key!==e),this.activeSection===e&&(this.activeSection=this.sectionKeys[0]||null),this.isMounted()&&await this.render(),this.emit("section:removed",{key:e}),!0):(console.warn(`SideNavView: Section "${e}" does not exist`),!1)}_onModelChange(){}static create(e={}){return new SideNavView(e)}}class FilePreviewView extends n.View{constructor(e={}){super({className:"file-preview",...e}),this.file=e.file||{},this.isImage=this.file.content_type?.startsWith("image/"),this.isPdf="application/pdf"===this.file.content_type}getTemplate(){return`\n <div class="file-preview-item card card-body p-2 mt-2">\n <div class="d-flex align-items-center">\n <div class="flex-shrink-0">\n ${this.isImage?`<img src="${this.file.thumbnailUrl||this.file.url}" class="rounded" style="width: 40px; height: 40px; object-fit: cover;">`:'<i class="bi bi-file-earmark-text fs-2 text-secondary"></i>'}\n </div>\n <div class="flex-grow-1 ms-3">\n <div class="fw-bold text-truncate">{{file.filename}}</div>\n <div class="small text-muted">{{file.file_size|filesize}}</div>\n </div>\n <div class="flex-shrink-0">\n <button class="btn btn-sm btn-outline-primary" data-action="view-file">View</button>\n </div>\n </div>\n </div>\n `}async onActionViewFile(){if(this.isImage){const e=window.MOJO?.plugins?.LightboxGallery;e?e.show({src:this.file.url,alt:this.file.filename}):window.open(this.file.url,"_blank")}else if(this.isPdf){const e=window.MOJO?.plugins?.PDFViewer;e?e.showDialog(this.file.url,{title:this.file.filename}):window.open(this.file.url,"_blank")}else window.open(this.file.url,"_blank")}}const r=[{value:"admin",label:"Admin"},{value:"email",label:"Email"},{value:"sms",label:"SMS"},{value:"push",label:"Push"},{value:"fileman",label:"File Manager"},{value:"api",label:"API"},{value:"other",label:"Other"}],c=[{value:"",label:"— None —"},{value:"summary",label:"summary"},{value:"summary_large_image",label:"summary_large_image"}];function d(e={}){const t={og_title:"og:title",og_description:"og:description",og_image:"og:image",twitter_card:"twitter:card",twitter_title:"twitter:title",twitter_description:"twitter:description",twitter_image:"twitter:image"},i={};for(const[s,n]of Object.entries(t)){const t=e[s];null!=t&&""!==t&&(i[n]=t)}return i}class ShortLink extends e.Model{constructor(e={}){super(e,{endpoint:"/api/shortlink/link"})}}class ShortLinkList extends e.Collection{constructor(e={}){super({ModelClass:ShortLink,endpoint:"/api/shortlink/link",...e})}}class ShortLinkClick extends e.Model{constructor(e={}){super(e,{endpoint:"/api/shortlink/history"})}}class ShortLinkClickList extends e.Collection{constructor(e={}){super({ModelClass:ShortLinkClick,endpoint:"/api/shortlink/history",...e})}}const h=[{name:"url",type:"url",label:"Destination URL",required:!0,placeholder:"https://example.com/page",cols:12},{name:"source",type:"select",label:"Source",options:r,value:"admin",cols:6},{name:"expire_days",type:"number",label:"Expire (days)",value:3,min:0,cols:3,help:"0 = never"},{name:"expire_hours",type:"number",label:"Expire (hours)",value:0,min:0,cols:3},{name:"track_clicks",type:"switch",label:"Track clicks",value:!1,cols:4,help:"Record per-click history and per-link metrics."},{name:"bot_passthrough",type:"switch",label:"Bypass bot preview",value:!1,cols:4,help:"Bots receive a plain redirect (use for transactional links)."},{name:"is_protected",type:"switch",label:"Protected",value:!1,cols:4,help:"Prevents accidental deletion."},{type:"heading",label:"OpenGraph Preview (optional)",cols:12},{name:"og_title",type:"text",label:"og:title",placeholder:"Shown in Slack/iMessage preview",cols:12},{name:"og_description",type:"textarea",label:"og:description",rows:2,cols:12},{name:"og_image",type:"url",label:"og:image",placeholder:"https://example.com/preview.jpg",cols:12}],m={create:{title:"Create Shortlink",size:"md",fields:[...h],help:"Leave OG fields blank to let the server scrape the destination automatically."},edit:{title:"Edit Shortlink",size:"md",fields:[{name:"is_active",type:"switch",label:"Active",cols:4},...h,{name:"twitter_card",type:"select",label:"twitter:card",options:c,cols:6},{name:"twitter_title",type:"text",label:"twitter:title",cols:6},{name:"twitter_description",type:"textarea",label:"twitter:description",rows:2,cols:12},{name:"twitter_image",type:"url",label:"twitter:image",cols:12}]}};ShortLink.EDIT_FORM=m.edit;const p={image:{icon:"bi-image",previewType:"image",badgeClass:"bg-info"},video:{icon:"bi-camera-video",previewType:"video",badgeClass:"bg-primary"},audio:{icon:"bi-music-note-beamed",previewType:"audio",badgeClass:"bg-primary"},pdf:{icon:"bi-file-earmark-pdf",previewType:"pdf",badgeClass:"bg-danger"},document:{icon:"bi-file-earmark-text",previewType:"document",badgeClass:"bg-secondary"},spreadsheet:{icon:"bi-file-earmark-spreadsheet",previewType:"document",badgeClass:"bg-success"},presentation:{icon:"bi-file-earmark-slides",previewType:"document",badgeClass:"bg-warning"},archive:{icon:"bi-file-earmark-zip",previewType:"download",badgeClass:"bg-dark"},other:{icon:"bi-file-earmark",previewType:"download",badgeClass:"bg-secondary"}};class FilePreviewSection extends n.View{constructor(e={}){super({className:"file-preview-section p-3",...e}),this.categoryConfig=e.categoryConfig||p.other}_onModelChange(){const e=this.categoryConfig?.previewType;"video"!==e&&"audio"!==e&&this.isMounted()&&this.render()}getTemplate(){const e=this.categoryConfig.previewType,t=this.model.get("url")||"",i=this.model.get("filename")||"";if("image"===e)return`\n <div class="text-center">\n <img src="${w(this.model.getThumbnailUrl&&this.model.getThumbnailUrl()||t)}"\n alt="${w(i)}"\n class="img-fluid rounded shadow-sm"\n style="max-height: 70vh; cursor: zoom-in;"\n data-action="view-file" role="button">\n <div class="text-muted small mt-2">Click image for full view</div>\n </div>\n `;if("video"===e){const e=this.model.getThumbnailUrl&&this.model.getThumbnailUrl();return`\n <div class="text-center">\n <video controls preload="metadata"\n src="${w(t)}"\n ${e?`poster="${w(e)}"`:""}\n style="width: 100%; max-height: 70vh; background:#000;"></video>\n </div>\n `}if("audio"===e)return`\n <div class="p-4 bg-light rounded text-center">\n <i class="bi ${this.categoryConfig.icon} display-4 text-secondary"></i>\n <h5 class="mt-3 mb-3 text-break">${g(i)}</h5>\n <audio controls class="w-100" src="${w(t)}"></audio>\n </div>\n `;if("pdf"===e)return`\n <div class="text-center p-5 bg-light rounded">\n <i class="bi ${this.categoryConfig.icon} text-danger" style="font-size: 5rem;"></i>\n <h5 class="mt-3 text-break">${g(i)}</h5>\n <div class="mt-4">\n <button type="button" class="btn btn-primary me-2" data-action="view-file">\n <i class="bi bi-eye me-1"></i>Open PDF Viewer\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="download-file">\n <i class="bi bi-download me-1"></i>Download\n </button>\n </div>\n </div>\n `;if("document"===e){const e=this.model.getBestImageRendition&&this.model.getBestImageRendition();return`\n <div class="text-center p-4 bg-light rounded">\n ${e?`<img src="${w(e.url)}" alt="${w(i)} preview" class="img-fluid rounded mb-3" style="max-height: 50vh;">`:`<i class="bi ${this.categoryConfig.icon} text-secondary" style="font-size: 5rem;"></i>`}\n <h5 class="mt-3 text-break">${g(i)}</h5>\n <div class="mt-3">\n <button type="button" class="btn btn-primary me-2" data-action="view-file">\n <i class="bi bi-box-arrow-up-right me-1"></i>Open\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="download-file">\n <i class="bi bi-download me-1"></i>Download\n </button>\n </div>\n </div>\n `}return`\n <div class="text-center p-5 bg-light rounded">\n <i class="bi ${this.categoryConfig.icon} text-secondary" style="font-size: 5rem;"></i>\n <h5 class="mt-3 text-break">${g(i)}</h5>\n <p class="text-muted small">No inline preview available for this file type.</p>\n <div class="mt-3">\n <button type="button" class="btn btn-primary" data-action="download-file">\n <i class="bi bi-download me-1"></i>Download\n </button>\n </div>\n </div>\n `}async onActionViewFile(){u(this.model,this.categoryConfig)}async onActionDownloadFile(){b(this.model)}}class FileRenditionsSection extends n.View{constructor(e={}){super({className:"file-renditions-section p-3",...e})}getTemplate(){return this.model.hasRenditions&&this.model.hasRenditions()?this._buildGalleryTemplate():this.model.isUploadPending&&this.model.isUploadPending()?this._buildWaitingTemplate():this._buildEmptyTemplate()}_buildGalleryTemplate(){const e=this.model.getRenditions(),t=e.map(e=>this._buildCard(e)).join(""),i=e.length;return`\n <div class="d-flex justify-content-between align-items-center mb-3">\n <div class="text-muted small">\n ${i} rendition${1===i?"":"s"}\n </div>\n <div class="btn-group btn-group-sm" role="group">\n <button type="button" class="btn btn-outline-secondary" data-action="refresh-renditions" title="Refresh list">\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="regenerate-from-section" title="Rebuild all previews">\n <i class="bi bi-arrow-repeat me-1"></i>Regenerate\n </button>\n </div>\n </div>\n <div class="row g-3">${t}</div>\n `}_buildCard(e){const t=e&&e.url?e.url:"",i=e&&"string"==typeof e.content_type?e.content_type:"",s=e&&e.role?e.role:"rendition",n=e&&e.filename?e.filename:s,a=e&&e.file_size?function(e){if(null==e||isNaN(e))return"";const t=Number(e);return t<1024?`${t} B`:t<1048576?`${(t/1024).toFixed(1)} KB`:t<1073741824?`${(t/1024/1024).toFixed(1)} MB`:`${(t/1024/1024/1024).toFixed(1)} GB`}(e.file_size):"",o=e&&e.width&&e.height?`${e.width} × ${e.height}`:"",l=['data-action="view-rendition"',`data-url="${w(t)}"`,`data-ct="${w(i)}"`,`data-filename="${w(n)}"`,`data-role="${w(s)}"`].join(" ");let r;r=i.startsWith("image/")&&t?`<img src="${w(t)}" alt="${w(s)}"\n loading="lazy"\n class="w-100"\n style="height: 140px; object-fit: cover; background: #f8f9fa; border-top-left-radius: var(--bs-card-inner-border-radius); border-top-right-radius: var(--bs-card-inner-border-radius);">`:i.startsWith("video/")?`\n <div class="d-flex align-items-center justify-content-center position-relative"\n style="height: 140px; background: linear-gradient(135deg, #212529 0%, #343a40 100%); color: #fff;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);">\n <i class="bi bi-play-circle-fill" style="font-size: 2.75rem; opacity: 0.9;"></i>\n <span class="position-absolute bottom-0 start-0 end-0 text-center small py-1"\n style="background: rgba(0,0,0,0.35); font-variant-numeric: tabular-nums;">\n ${g(i)}\n </span>\n </div>`:i.startsWith("audio/")?'\n <div class="d-flex align-items-center justify-content-center"\n style="height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);">\n <i class="bi bi-music-note-beamed text-secondary" style="font-size: 2.5rem;"></i>\n </div>':'\n <div class="d-flex align-items-center justify-content-center"\n style="height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);">\n <i class="bi bi-file-earmark text-secondary" style="font-size: 2.5rem;"></i>\n </div>';const c=t?`\n <div class="card-footer p-1 d-flex gap-1 bg-white border-top-0">\n <button type="button" class="btn btn-sm btn-outline-primary flex-fill"\n ${l}\n title="Preview">\n <i class="bi bi-eye"></i>\n </button>\n <button type="button" class="btn btn-sm btn-outline-secondary flex-fill"\n data-action="copy-rendition-url" data-url="${w(t)}"\n title="Copy URL">\n <i class="bi bi-clipboard"></i>\n </button>\n <a href="${w(t)}" download="${w(n)}"\n class="btn btn-sm btn-outline-secondary flex-fill"\n title="Download"\n data-stop-propagation>\n <i class="bi bi-download"></i>\n </a>\n </div>\n `:"";return`\n <div class="col-sm-6 col-md-4 col-lg-3">\n <div class="card h-100 shadow-sm rendition-card">\n <div ${t?l:""} ${t?'role="button" style="cursor: pointer;"':""}>\n ${r}\n </div>\n <div class="card-body p-2">\n <div class="d-flex justify-content-between align-items-start gap-2 mb-1">\n <span class="badge bg-secondary text-truncate" style="max-width: 100%;" title="${w(s)}">${g(s)}</span>\n ${a?`<small class="text-muted flex-shrink-0" style="font-variant-numeric: tabular-nums;">${g(a)}</small>`:""}\n </div>\n ${o?`<div class="small text-muted" style="font-variant-numeric: tabular-nums;">${g(o)}</div>`:""}\n </div>\n ${c}\n </div>\n </div>\n `}_buildEmptyTemplate(){return'\n <div class="text-center p-5 bg-light rounded">\n <i class="bi bi-layers display-6 text-muted"></i>\n <h6 class="mt-3 mb-1">No renditions for this file</h6>\n <p class="text-muted small mb-3">\n Click <strong>Regenerate</strong> to (re)build thumbnails and previews on the backend.\n </p>\n <div class="d-inline-flex gap-2">\n <button type="button" class="btn btn-sm btn-outline-secondary" data-action="refresh-renditions">\n <i class="bi bi-arrow-clockwise me-1"></i>Refresh\n </button>\n <button type="button" class="btn btn-sm btn-outline-primary" data-action="regenerate-from-section">\n <i class="bi bi-arrow-repeat me-1"></i>Regenerate\n </button>\n </div>\n </div>\n '}_buildWaitingTemplate(){return'\n <div class="text-center p-5 bg-light rounded">\n <i class="bi bi-hourglass-split display-6 text-muted"></i>\n <h6 class="mt-3 mb-1">Upload still in progress</h6>\n <p class="text-muted small mb-0">Renditions will be generated once the upload completes.</p>\n </div>\n '}async onActionRefreshRenditions(){try{await this.model.fetch()}catch(e){console.warn("FileView: refresh-renditions fetch failed:",e)}}async onActionViewRendition(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t.dataset.url,s=t.dataset.ct||"";if(i){if(s.startsWith("image/")){const e="undefined"!=typeof window?window.MOJO?.plugins?.LightboxGallery:null;if(e&&"function"==typeof e.show){const t=this.model.getRenditions().filter(e=>e&&e.url&&"string"==typeof e.content_type&&e.content_type.startsWith("image/")).map(e=>({src:e.url,alt:e.role||""})),s=Math.max(0,t.findIndex(e=>e.src===i));return void e.show(t,{startIndex:s,fitToScreen:!1})}}window.open(i,"_blank","noopener")}}async onActionCopyRenditionUrl(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t.dataset.url;if(i)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(i);else{const e=document.createElement("textarea");e.value=i,document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}const e=t.querySelector("i");if(e){const t=e.className;e.className="bi bi-check-lg text-success",setTimeout(()=>{e.className=t},1200)}this.getApp()?.toast?.success?.("URL copied to clipboard")}catch(s){console.error("Failed to copy rendition URL:",s),this.getApp()?.toast?.error?.("Failed to copy URL")}}async onActionRegenerateFromSection(){let e=this.parent;for(;e;){if("function"==typeof e.onActionRegenerateRenditions)return e.onActionRegenerateRenditions();e=e.parent}}}function u(e,t){const i=e.get("url");if(!i)return;const s=t.previewType;if("image"===s){const t="undefined"!=typeof window?window.MOJO?.plugins?.LightboxGallery:null,s=e.get("renditions")||{},n=[{src:i,alt:"Original"},...Object.values(s).filter(e=>e&&e.url&&"string"==typeof e.content_type&&e.content_type.startsWith("image/")).map(e=>({src:e.url,alt:e.role||""}))];return void(t&&"function"==typeof t.show?t.show(n,{fitToScreen:!1}):window.open(i,"_blank"))}if("pdf"===s){const t="undefined"!=typeof window?window.MOJO?.plugins?.PDFViewer:null;return void(t&&"function"==typeof t.showDialog?t.showDialog(i,{title:e.get("filename")}):window.open(i,"_blank"))}window.open(i,"_blank")}function b(e){const t=e.get("url");if(!t)return;const i=document.createElement("a");i.href=t,i.download=e.get("filename")||"",document.body.appendChild(i),i.click(),document.body.removeChild(i)}class FileSharesSection extends n.View{constructor(e={}){super({className:"file-shares-section p-3",...e}),this.template='\n <div class="d-flex justify-content-between align-items-center mb-3">\n <div class="text-muted small">\n Active and historical shareable links for this file.\n </div>\n <button type="button" class="btn btn-sm btn-primary"\n data-action="share-file-from-section">\n <i class="bi bi-link-45deg me-1"></i>Share new\n </button>\n </div>\n <div data-container="file-shares-table"></div>\n '}async onInit(){const e=this.model.get("id");if(!e)return;const t=new ShortLinkList({params:{source:"fileman-share",file:e,sort:"-created",size:25}});this._sharesCollection=t,this.sharesTable=new s.TableView({containerId:"file-shares-table",collection:t,hideActivePillNames:["source","file"],columns:[{key:"code",label:"Short URL",template:'\n <div class="d-flex align-items-center gap-2">\n <code>{{model.code}}</code>\n <button class="btn btn-sm btn-link p-0 text-muted"\n data-action="copy-share-code"\n data-code="{{model.code}}"\n title="Copy short URL">\n <i class="bi bi-clipboard"></i>\n </button>\n </div>\n '},{key:"user.display_name",label:"Shared By",formatter:"default('—')"},{key:"hit_count",label:"Hits",width:"80px",sortable:!0},{key:"track_clicks",label:"Tracked",width:"90px",formatter:"yesnoicon"},{key:"is_active",label:"Active",width:"80px",formatter:"yesnoicon"},{key:"expires_at",label:"Expires",width:"160px",formatter:"datetime|default('Never')",sortable:!0},{key:"created",label:"Created",width:"160px",formatter:"datetime",sortable:!0},{key:"metadata.note",label:"Note",formatter:"truncate(40)|default('—')",visibility:"lg"}],paginated:!0,sortable:!0,searchable:!1,filterable:!1,contextMenu:[{label:"Copy Short URL",action:"copy-share-code",icon:"bi-clipboard"},{divider:!0},{label:"Revoke",action:"revoke-share",icon:"bi-slash-circle",danger:!0}],tableOptions:{hover:!0,size:"sm",emptyMessage:"No shares yet — click “Share new” to mint a tracked link.",emptyIcon:"bi-link-45deg",actions:[]}}),this.addChild(this.sharesTable)}refreshShares(){return this._sharesCollection?.fetch()}async onActionShareFileFromSection(){let e=this.parent;for(;e;){if("function"==typeof e.onActionShareFile)return e.onActionShareFile();e=e.parent}return null}async onActionCopyShareCode(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t?.dataset?.code;if(!i)return;const s=function(e,t){if(!e)return"";const i=t?.config?.shortlink_base_url||("undefined"!=typeof window?window.location.origin:"");return`${String(i).replace(/\/+$/,"")}/s/${e}`}(i,this.getApp?.());try{await navigator.clipboard.writeText(s),this.getApp()?.toast?.success?.(`Copied: ${s}`)}catch(n){this.getApp()?.toast?.warning?.("Copy failed — select the URL manually.")}}async onActionRevokeShare(e,t){e&&(e.preventDefault(),e.stopPropagation());const s=t?.closest?.("[data-row-id]"),n=s?.dataset?.rowId||t?.dataset?.id;if(!n)return;const a=this._sharesCollection?.get?.(n);if(a&&await i.Modal.confirm("Revoke this share? Anyone with the short URL will get a 404 — the audit row is preserved.","Revoke Share",{confirmText:"Revoke",confirmClass:"btn-danger"}))try{await a.save({is_active:!1}),this.getApp()?.toast?.success?.("Share revoked"),await this.refreshShares()}catch(o){console.error("Failed to revoke share:",o),this.getApp()?.toast?.error?.("Failed to revoke share")}}}class FileView extends n.View{constructor(e={}){super({className:"file-view",...e}),this.model=e.model||new i.File(e.data||{}),this.sideNavView=null,this.contextMenu=null,this.template='\n <div class="file-view-container d-flex flex-column" style="min-height: 0;">\n \x3c!-- Header + Context Menu --\x3e\n <div class="d-flex justify-content-between align-items-start mb-3 flex-shrink-0">\n <div data-container="file-header" style="flex: 1; min-width: 0;"></div>\n <div data-container="file-context-menu" class="ms-3 flex-shrink-0"></div>\n </div>\n \x3c!-- Section body --\x3e\n <div data-container="file-sidenav" class="flex-grow-1" style="min-height: 400px; max-height: 70vh;"></div>\n </div>\n '}_getCategoryConfig(){return function(e){const t=e&&"function"==typeof e.getCategory?e.getCategory():e?.get?.("category")||"other";return p[t]||p.other}(this.model)}async onInit(){const e=this._getCategoryConfig();this.header=new n.View({containerId:"file-header",template:this._buildHeaderTemplate(e)}),this.header.setModel(this.model),this.addChild(this.header);const i=[],s=new FilePreviewSection({model:this.model,categoryConfig:e});i.push({key:"preview",label:"Preview",icon:e.icon,view:s});const o=new a.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"ID"},{name:"filename",label:"Filename"},{name:"storage_filename",label:"Storage Filename"},{name:"content_type",label:"Content Type"},{name:"file_size",label:"File Size",format:"filesize"},{name:"category",label:"Category"},{name:"upload_status",label:"Status",format:"badge"},{name:"created",label:"Created",format:"datetime"},{name:"modified",label:"Modified",format:"datetime"},{name:"user.display_name",label:"Uploaded By"},{name:"file_manager.name",label:"Storage Backend"},{name:"storage_file_path",label:"Storage Path"},{name:"url",label:"Public URL",format:"url"},{name:"is_public",label:"Is Public",format:"boolean"}]});i.push({key:"details",label:"Details",icon:"bi-info-circle",view:o});const l=new FileRenditionsSection({model:this.model});i.push({key:"renditions",label:"Renditions",icon:"bi-layers",view:l}),this.model.get("id")&&(this.sharesSection=new FileSharesSection({model:this.model}),i.push({key:"shares",label:"Shares",icon:"bi-link-45deg",view:this.sharesSection}));const r=this.model.get("metadata");if(r&&"object"==typeof r&&Object.keys(r).length){const e=new a.default({data:r,className:"p-3",columns:2,showEmptyValues:!1});i.push({key:"metadata",label:"Metadata",icon:"bi-braces",view:e})}this.sideNavView=new SideNavView({containerId:"file-sidenav",activeSection:"preview",navWidth:200,contentPadding:"1.25rem 1.5rem",enableResponsive:!0,minWidth:500,sections:i}),this.addChild(this.sideNavView),this.contextMenu=new t.ContextMenu({containerId:"file-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View",action:"view-file",icon:"bi-eye"},{label:"Download",action:"download-file",icon:"bi-download"},{label:"Copy URL",action:"copy-url",icon:"bi-clipboard"},{label:"Share Link…",action:"share-file",icon:"bi-link-45deg"},{type:"divider"},{label:"Edit Details",action:"edit-file",icon:"bi-pencil"},this.model.get("is_public")?{label:"Make Private",action:"make-private",icon:"bi-lock"}:{label:"Make Public",action:"make-public",icon:"bi-unlock"},{label:"Regenerate Previews",action:"regenerate-renditions",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete File",action:"delete-file",icon:"bi-trash",danger:!0}]}}),this.addChild(this.contextMenu)}async onBeforeDestroy(){this._stopRenditionsPoll()}_buildHeaderTemplate(e){const t=this.model.getThumbnailUrl&&this.model.getThumbnailUrl();return`\n <div class="d-flex align-items-center gap-3">\n <div class="file-view-thumb flex-shrink-0">\n ${t?`<img src="${w(t)}" alt="thumbnail" class="rounded" style="width: 80px; height: 80px; object-fit: cover;">`:`<div class="rounded bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi ${e.icon} text-secondary" style="font-size: 2.25rem;"></i>\n </div>`}\n </div>\n <div class="flex-grow-1" style="min-width:0;">\n <h3 class="mb-1 text-break">{{model.filename|default('Unnamed file')}}</h3>\n <div class="text-muted small d-flex flex-wrap align-items-center gap-2">\n <span><i class="bi bi-hdd me-1"></i>{{model.file_size|filesize}}</span>\n <span class="text-muted">·</span>\n <span>{{model.content_type|default('unknown')}}</span>\n <span class="text-muted">·</span>\n <span class="badge ${e.badgeClass}">{{model.category|default('other')|capitalize}}</span>\n {{#model.upload_status|bool}}\n <span class="text-muted">·</span>\n <span class="badge {{model.upload_status|badge}}">{{model.upload_status|capitalize}}</span>\n {{/model.upload_status|bool}}\n {{#model.is_public|bool}}\n <span class="text-muted">·</span>\n <span class="badge bg-success"><i class="bi bi-unlock me-1"></i>Public</span>\n {{/model.is_public|bool}}\n {{^model.is_public|bool}}\n <span class="text-muted">·</span>\n <span class="badge bg-secondary"><i class="bi bi-lock me-1"></i>Private</span>\n {{/model.is_public|bool}}\n </div>\n {{#model.created|bool}}\n <div class="text-muted small mt-1">\n Uploaded {{model.created|epoch|datetime}}\n </div>\n {{/model.created|bool}}\n </div>\n </div>\n `}async onActionViewFile(){u(this.model,this._getCategoryConfig())}async onActionDownloadFile(){b(this.model)}async onActionCopyUrl(){const e=this.model.get("url");if(e)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(e);else{const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}this.getApp()?.toast?.success?.("URL copied to clipboard")}catch(t){console.error("Failed to copy URL:",t),this.getApp()?.toast?.error?.("Failed to copy URL")}}async onActionEditFile(){await i.Modal.modelForm({title:`Edit File - ${this.model.get("filename")}`,model:this.model,formConfig:i.FileForms.edit})&&this.render()}async onActionMakePublic(){await this.model.save({is_public:!0}),this.render()}async onActionMakePrivate(){await this.model.save({is_public:!1}),this.render()}async onActionShareFile(){if(!this.model.get("id"))return void this.getApp()?.toast?.warning?.("Save the file before sharing it.");const e=await i.Modal.form({title:"Share Link",size:"sm",help:"Each share creates a distinct, audit-tracked short URL attributed to you.",fields:[{name:"expire_days",type:"number",label:"Expire after (days)",value:30,min:0,cols:12,help:"0 = never expires. Server max: 3650."},{name:"track_clicks",type:"switch",label:"Track clicks",value:!0,cols:12,help:"Records per-click history (IP, user-agent, bot/human)."},{name:"note",type:"textarea",label:"Note (optional)",rows:2,cols:12,maxlength:512,help:"Private audit note — not shown to recipients."}],submitText:"Share"});if(!e)return;const t={};let s;void 0!==e.expire_days&&null!==e.expire_days&&""!==e.expire_days&&(t.expire_days=Number(e.expire_days)),void 0!==e.track_clicks&&(t.track_clicks=!!e.track_clicks),e.note&&(t.note=String(e.note).slice(0,512));try{s=await this.model.share(!Object.keys(t).length||t)}catch(h){return console.error("Share failed:",h),void i.Modal.showError(h?.data?.error||h?.message||"Failed to create share link")}const n=s?.data,a=n?.url;if(!s?.success||!a)return void i.Modal.showError(n?.error||"Failed to create share link");let o=!1;try{await(navigator.clipboard?.writeText?.(a)),o=!0}catch(m){o=!1}const l=n.expires_at?new Date(n.expires_at).toLocaleString():"Never",r=n.track_clicks?"Yes":"No",c=o?'<div class="form-text text-success mb-2"><i class="bi bi-check-circle me-1"></i>Copied to clipboard.</div>':'<div class="form-text text-muted mb-2">Select the URL above to copy.</div>',d=`\n <div class="mb-2">\n <label class="form-label small text-muted mb-1">Share URL</label>\n <input type="text" class="form-control font-monospace" readonly value="${w(a)}">\n ${c}\n </div>\n <dl class="row small mb-0">\n <dt class="col-4 text-muted">Expires</dt><dd class="col-8">${g(l)}</dd>\n <dt class="col-4 text-muted">Tracked</dt><dd class="col-8">${g(r)}</dd>\n ${n.shortlink_code?`<dt class="col-4 text-muted">Code</dt><dd class="col-8"><code>${g(n.shortlink_code)}</code></dd>`:""}\n </dl>\n `;await i.Modal.alert(d,"Share link created",{type:"success"});try{await(this.sharesSection?.refreshShares?.())}catch(h){console.warn("Failed to refresh shares section:",h)}}async onActionRegenerateRenditions(){if(await i.Modal.confirm("Rebuild all previews and thumbnails for this file? Existing renditions will be replaced. Generation runs in the background and may take several minutes for video.","Regenerate Previews",{confirmText:"Regenerate"})){try{await this.model.regenerateRenditions(),this.getApp()?.toast?.success?.("Regenerating previews in the background…")}catch(e){return console.error("Failed to trigger regenerate_renditions:",e),void this.getApp()?.toast?.error?.("Failed to start preview regeneration")}this._maybeStartRenditionsPoll({force:!0})}}_maybeStartRenditionsPoll(e={}){if(this._renditionsPollTimer)return;if(!e.force)return;let t=0;const i=()=>{this._renditionsPollTimer=null,this.model&&(this.model.hasRenditions&&this.model.hasRenditions()||++t>60||(this._renditionsPollTimer=setTimeout(async()=>{try{await this.model.fetch()}catch(e){console.warn("FileView: renditions poll fetch failed:",e)}i()},5e3)))};i()}_stopRenditionsPoll(){this._renditionsPollTimer&&(clearTimeout(this._renditionsPollTimer),this._renditionsPollTimer=null)}async onActionDeleteFile(){if(!(await i.Modal.confirm(`Are you sure you want to delete the file "${this.model.get("filename")}"? This action cannot be undone.`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})))return;const e=await this.model.destroy();e&&e.success&&this.emit("file:deleted",{model:this.model})}_onModelChange(){}async showSection(e){this.sideNavView&&await this.sideNavView.showSection(e)}getActiveSection(){return this.sideNavView?this.sideNavView.getActiveSection():null}static create(e={}){return new FileView(e)}}function g(e){return null==e?"":String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function w(e){return g(e)}i.File.VIEW_CLASS=FileView;class ChatMessageView extends n.View{constructor(e={}){const t=e.message||{},i=e.theme||"compact",s=e.isCurrentUser||!1,n=t.role||(s?"user":null);let a="chat-message";"bubbles"===i&&(a+=s?" message-right":" message-left"),"assistant"===n?a+=" message-assistant":"user"===n&&(a+=" message-user"),super({className:a,...e}),this.message=t,this.theme=i,this.isCurrentUser=s,this.role=n}getTemplate(){return"system_event"===this.message.type?'\n <div class="chat-message-system text-center text-muted small py-2">\n <i class="bi bi-info-circle me-1"></i>\n {{message.content}}\n </div>\n ':"bubbles"===this.theme?this.getBubblesTemplate():this.getCompactTemplate()}getCompactTemplate(){const e=this.isCurrentUser?"bg-primary":"bg-secondary",t="assistant"===this.role;return`\n <div class="message-item">\n <div class="message-avatar ${t?"bg-dark":e}">\n ${t?'<i class="bi bi-robot"></i>':'\n {{#message.author.avatarUrl}}\n <img src="{{message.author.avatarUrl}}" alt="{{message.author.name}}" class="w-100 h-100 rounded-circle">\n {{/message.author.avatarUrl}}\n {{^message.author.avatarUrl}}\n {{message.author.name|initials}}\n {{/message.author.avatarUrl}}\n '}\n </div>\n <div class="message-content">\n <div class="message-header">\n <div class="message-author">\n ${t?"Assistant":"{{message.author.name}}"}\n {{#isCurrentUser}}\n <span class="badge bg-primary badge-sm ms-1">You</span>\n {{/isCurrentUser}}\n </div>\n <div class="message-time text-muted">{{message.timestamp|relative}}</div>\n </div>\n <div class="message-text">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container="blocks-${this.message.id||this.id}"></div>\n <div data-container="attachments"></div>\n </div>\n </div>\n `}getBubblesTemplate(){return`\n <div class="message-bubble-wrapper">\n <div class="message-meta">\n <strong>${"assistant"===this.role?'<i class="bi bi-robot me-1"></i>Assistant':"{{message.author.name}}"}</strong>\n <span class="text-muted">· {{message.timestamp|relative}}</span>\n </div>\n <div class="message-bubble">\n <div class="message-text">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container="blocks-${this.message.id||this.id}"></div>\n <div data-container="attachments"></div>\n </div>\n </div>\n `}_getToolCallsTemplate(){if(!this.message.tool_calls||0===this.message.tool_calls.length)return"";const e=this.message.tool_calls.map(e=>{const t=(e=>{const t=document.createElement("div");return t.textContent=e,t.innerHTML})(e.name||e.function?.name||"tool");return`<span class="badge ${"error"===e.status?"bg-danger":"bg-info"} me-1">${t}</span>`}).join(""),t=`tools-${this.message.id||this.id}`;return`\n <div class="message-tool-calls mt-1">\n <a class="text-muted small" data-bs-toggle="collapse" href="#${t}" role="button" aria-expanded="false">\n <i class="bi bi-tools me-1"></i>${this.message.tool_calls.length} tool call${this.message.tool_calls.length>1?"s":""}\n </a>\n <div class="collapse" id="${t}">\n <div class="mt-1">${e}</div>\n </div>\n </div>\n `}async onAfterRender(){if(this.message.attachments&&this.message.attachments.length>0){const e=this.element.querySelector('[data-container="attachments"]');e&&this.message.attachments.forEach(t=>{const i=new FilePreviewView({file:t});this.addChild(i),i.render(!0,e)})}}}class ChatInputView extends n.View{constructor(e={}){super({className:"chat-input-view",...e}),this.placeholder=e.placeholder||"Type a message...",this.buttonText=e.buttonText||"Send",this.showFileInput=!1!==e.showFileInput,this.attachments=[],this.pendingUploads=/* @__PURE__ */new Map}getTemplate(){return`\n <div class="chat-input-container">\n <div class="chat-input-attachments" data-container="attachments"></div>\n <div class="chat-input-wrapper">\n <textarea\n class="chat-input form-control"\n placeholder="${this.placeholder}"\n rows="2"></textarea>\n <button class="chat-send-btn btn btn-primary" data-action="send-message" type="button">\n <i class="bi bi-send-fill"></i>\n <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>\n </button>\n </div>\n ${this.showFileInput?'\n <div class="chat-input-footer">\n <small class="text-muted">\n <i class="bi bi-paperclip"></i>\n Drag & drop files to attach\n </small>\n </div>\n ':""}\n </div>\n `}async onAfterRender(){this.showFileInput&&this.enableFileDrop({dropZoneSelector:".chat-input-container",multiple:!0,acceptedTypes:["*/*"],visualFeedback:!0,dragOverClass:"drag-over",dragActiveClass:"drag-active"});const e=this.element.querySelector(".chat-input");e&&(e.addEventListener("input",()=>this.autoResizeTextarea(e)),e.addEventListener("keydown",e=>this.handleKeydown(e)))}handleKeydown(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),this.onActionSendMessage(e,e.target))}async onFileDrop(e){for(const t of e)await this.uploadFile(t)}async uploadFile(e){const t=new i.File,s=Date.now()+Math.random();this.addFilePreview(s,e,0),this.pendingUploads.set(s,{file:e,fileModel:t});try{await t.upload({file:e,onProgress:e=>{this.updateFileProgress(s,e)},onComplete:e=>{this.handleUploadComplete(s,t)}})}catch(n){console.error("File upload failed:",n),this.handleUploadError(s,n)}}addFilePreview(e,t,i){const s=this.element.querySelector('[data-container="attachments"]');if(!s)return;const n=document.createElement("div");n.className="attachment-preview",n.dataset.uploadId=e,n.innerHTML=`\n <div class="attachment-info">\n <i class="bi bi-file-earmark"></i>\n <span class="attachment-name">${this.escapeHtml(t.name)}</span>\n <span class="attachment-size">(${this.formatFileSize(t.size)})</span>\n </div>\n <div class="attachment-progress">\n <div class="progress" style="height: 4px;">\n <div class="progress-bar" role="progressbar" style="width: ${i}%"></div>\n </div>\n </div>\n <button class="attachment-remove btn btn-sm btn-link text-danger" data-action="remove-attachment" data-upload-id="${e}" type="button">\n <i class="bi bi-x"></i>\n </button>\n `,s.appendChild(n)}updateFileProgress(e,t){const i=this.element.querySelector(`[data-upload-id="${e}"]`);if(i){const e=i.querySelector(".progress-bar");e&&(e.style.width=`${t}%`)}}handleUploadComplete(e,t){this.attachments.push({id:t.id,name:t.get("name"),uploadId:e}),this.pendingUploads.delete(e);const i=this.element.querySelector(`[data-upload-id="${e}"]`);if(i){i.classList.add("upload-complete");const e=i.querySelector(".attachment-progress");e&&e.remove()}}handleUploadError(e,t){this.pendingUploads.delete(e);const i=this.element.querySelector(`[data-upload-id="${e}"]`);i&&(i.classList.add("upload-error"),i.querySelector(".attachment-info").innerHTML+='<span class="text-danger ms-2">Upload failed</span>')}async onActionRemoveAttachment(e,t){const i=t.dataset.uploadId;this.pendingUploads.delete(i);const s=this.element.querySelector(`[data-upload-id="${i}"]`);s&&s.remove()}async onActionSendMessage(e,t){const i=this.element.querySelector(".chat-input").value.trim();(i||0!==this.attachments.length)&&(this.pendingUploads.size>0||(this.setBusy(!0),this.emit("message:send",{text:i,files:this.attachments})))}setEnabled(e){const t=this.element?.querySelector(".chat-input"),i=this.element?.querySelector(".chat-send-btn");t&&(t.disabled=!e),i&&(i.disabled=!e)}setBusy(e){const t=this.element.querySelector(".chat-send-btn"),i=t.querySelector(".bi-send-fill"),s=t.querySelector(".spinner-border");e?(t.disabled=!0,i.classList.add("d-none"),s.classList.remove("d-none")):(t.disabled=!1,i.classList.remove("d-none"),s.classList.add("d-none"))}clearInput(){const e=this.element.querySelector(".chat-input");e&&(e.value="",e.style.height="auto");const t=this.element.querySelector('[data-container="attachments"]');t&&(t.innerHTML=""),this.attachments=[],this.pendingUploads.clear(),this.setBusy(!1)}autoResizeTextarea(e){e.style.height="auto",e.style.height=Math.min(e.scrollHeight,150)+"px"}formatFileSize(e){if(0===e)return"0 B";const t=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,t)).toFixed(1))+" "+["B","KB","MB","GB"][t]}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}}o.applyFileDropMixin(ChatInputView);class ChatView extends n.View{constructor(e={}){super({className:"chat-view",...e}),this.adapter=e.adapter,this.theme=e.theme||"compact",this.currentUserId=e.currentUserId,this.inputPlaceholder=e.inputPlaceholder||"Type a message...",this.inputButtonText=e.inputButtonText||"Send",this.showFileInput=!1!==e.showFileInput,this.showInput=!1!==e.showInput,this.MessageViewClass=e.messageViewClass||ChatMessageView,this.messages=[],this.messageViews=/* @__PURE__ */new Map,this._thinkingEl=null}getTemplate(){return`\n <div class="chat-container chat-theme-${this.theme}">\n <div class="chat-messages" data-container="messages"></div>\n ${this.showInput?'<div class="chat-input-wrapper" data-container="input"></div>':""}\n </div>\n `}async onInit(){this.messages=await this.adapter.fetch(),this.showInput&&(this.inputView=new ChatInputView({containerId:"input",placeholder:this.inputPlaceholder,buttonText:this.inputButtonText,showFileInput:this.showFileInput}),this.addChild(this.inputView),this.inputView.on("message:send",async e=>{await this.handleSendMessage(e)}))}async onAfterRender(){this._buildMessageViews(),await this._renderChildren(),this.scrollToBottom()}async _renderChildren(){const e=/* @__PURE__ */new Set;this.messageViews.forEach(t=>e.add(t.id));for(const i in this.children){const t=this.children[i];t&&!e.has(i)&&(t.parent=this,await Promise.resolve(t.render()).catch(e=>console.warn(`ChatView child render error (${i})`,e)))}const t=this.element.querySelector('[data-container="messages"]');t&&this.messageViews.forEach(e=>{t.appendChild(e.element),e.render(!1)})}_buildMessageViews(){this.messages&&0!==this.messages.length&&this.messages.forEach(e=>{this.messageViews.has(e.id)||this._createMessageView(e)})}_createMessageView(e){if(this.messageViews.has(e.id))return;const t=e.author&&e.author.id===this.currentUserId,i=new this.MessageViewClass({message:e,theme:this.theme,isCurrentUser:t});return this.addChild(i),this.messageViews.set(e.id,i),i}addMessage(e,t=!0){if(this.messageViews.has(e.id))return;const i=this._createMessageView(e);if(this.isMounted()){const e=this.element.querySelector('[data-container="messages"]');e&&(e.appendChild(i.element),i.render(!1))}t&&this.scrollToBottom()}async handleSendMessage(e){try{if(e.text&&e.text.trim()&&!(await this.adapter.addNote({text:e.text,files:e.files&&e.files.length>0?[e.files[0]]:[]})).success)throw new Error("Failed to send message");for(let t=e.text&&e.text.trim()&&e.files?.length>0?1:0;t<(e.files?.length||0);t++){const i=e.files[t];(await this.adapter.addNote({text:"",files:[i]})).success||console.error("Failed to upload file:",i)}this.messages=await this.adapter.fetch(),this.messages.forEach(e=>{this.messageViews.has(e.id)||this.addMessage(e,!0)}),this.inputView.clearInput()}catch(t){console.error("Failed to send message:",t),this.inputView.setBusy(!1);try{this.getApp().toast.error("Failed to send message")}catch(i){}}}showThinking(e="Thinking..."){const t=this.element?.querySelector('[data-container="messages"]');t&&(this._thinkingEl||(this._thinkingEl=document.createElement("div"),this._thinkingEl.className="chat-thinking",this._thinkingEl.innerHTML='\n <div class="chat-thinking-content">\n <span class="chat-thinking-dots">\n <span></span><span></span><span></span>\n </span>\n <span class="chat-thinking-text"></span>\n <span class="chat-thinking-timer text-muted"></span>\n </div>\n ',t.appendChild(this._thinkingEl),this._thinkingStart=Date.now(),this._thinkingInterval=setInterval(()=>{const e=Math.floor((Date.now()-this._thinkingStart)/1e3),t=Math.floor(e/60),i=e%60,s=this._thinkingEl?.querySelector(".chat-thinking-timer");s&&(s.textContent=t>0?`${t}m ${String(i).padStart(2,"0")}s`:`${i}s`)},1e3)),this._thinkingEl.querySelector(".chat-thinking-text").textContent=e,this.scrollToBottom())}hideThinking(){this._thinkingInterval&&(clearInterval(this._thinkingInterval),this._thinkingInterval=null),this._thinkingEl&&(this._thinkingEl.remove(),this._thinkingEl=null)}setInputEnabled(e){this.inputView?.setEnabled&&this.inputView.setEnabled(e)}scrollToBottom(){const e=this.element.querySelector(".chat-messages");e&&requestAnimationFrame(()=>{e.scrollTop=e.scrollHeight})}clearMessages(){this.messageViews.forEach(e=>{delete this.children[e.id],e.destroy()}),this.messageViews.clear(),this.messages=[];const e=this.element?.querySelector('[data-container="messages"]');e&&(e.innerHTML="")}async refresh(){this.clearMessages(),this.messages=await this.adapter.fetch(),this._buildMessageViews(),this.isMounted()&&(await this._renderChildren(),this.scrollToBottom())}}exports.ChatInputView=ChatInputView,exports.ChatMessageView=ChatMessageView,exports.ChatView=ChatView,exports.FilePreviewView=FilePreviewView,exports.FileView=FileView,exports.GeoLocatedIP=GeoLocatedIP,exports.GeoLocatedIPList=GeoLocatedIPList,exports.MetricsForms={edit:{title:"Edit Metrics Permissions",fields:[{name:"account",type:"text",label:"Account",columns:12},{name:"view_permissions",type:"tags",label:"View Permissions",help:'Enter permissions or "public"',columns:12},{name:"write_permissions",type:"tags",label:"Write Permissions",help:"Enter permissions",columns:12}]}},exports.MetricsPermission=MetricsPermission,exports.MetricsPermissionList=MetricsPermissionList,exports.SHORTLINK_SOURCE_OPTIONS=r,exports.ShortLink=ShortLink,exports.ShortLinkClickList=ShortLinkClickList,exports.ShortLinkForms=m,exports.ShortLinkList=ShortLinkList,exports.SideNavView=SideNavView,exports.TWITTER_CARD_OPTIONS=c,exports.TabView=TabView,exports.TablePage=TablePage,exports.buildShortLinkMetadata=d,exports.extractShortLinkPayload=function(e={}){const t=["og_title","og_description","og_image","twitter_card","twitter_title","twitter_description","twitter_image"],i={...e};for(const n of t)delete i[n];const s=d(e);return Object.keys(s).length>0&&(i.metadata=s),i},exports.flattenShortLinkMetadata=function(e={}){const t=e||{};return{og_title:t["og:title"]||"",og_description:t["og:description"]||"",og_image:t["og:image"]||"",twitter_card:t["twitter:card"]||"",twitter_title:t["twitter:title"]||"",twitter_description:t["twitter:description"]||"",twitter_image:t["twitter:image"]||""}};
2
+ //# sourceMappingURL=ChatView-B73uox2v.js.map