@conduction/nextcloud-vue 2.142.0 → 2.144.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 (167) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
  12. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
  14. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
  15. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
  16. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  18. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
  19. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
  21. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  22. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  23. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  25. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  27. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  29. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  30. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  31. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  32. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  33. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  34. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
  35. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  40. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  41. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
  42. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  43. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
  44. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +29 -3
  47. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +4 -2
  49. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  51. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  52. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  53. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +18 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  57. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  58. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
  59. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  60. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
  61. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  63. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
  64. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
  66. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  68. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  69. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  76. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
  78. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  80. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  82. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  83. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
  84. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
  86. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  87. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  88. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  89. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  90. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  91. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  92. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  93. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
  94. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  95. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
  96. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  97. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  98. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
  99. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  100. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  101. package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
  102. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
  103. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  104. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
  105. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  106. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
  107. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  108. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  109. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
  110. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  111. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  112. package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
  113. package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
  114. package/dist/esm/l10n/en.json.js +6 -0
  115. package/dist/esm/l10n/en.json.js.map +1 -1
  116. package/dist/esm/l10n/nl.json.js +7 -1
  117. package/dist/esm/l10n/nl.json.js.map +1 -1
  118. package/dist/esm/library-style.js +1 -1
  119. package/dist/esm/nextcloud-vue.css +1264 -597
  120. package/dist/esm/utils/fetchFilterCounts.js +5 -2
  121. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  122. package/dist/esm/utils/nextcloudDirectory.js +19 -6
  123. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  124. package/dist/nextcloud-vue.cjs.js +1619 -849
  125. package/dist/nextcloud-vue.cjs.js.map +2 -2
  126. package/dist/nextcloud-vue.css +1264 -597
  127. package/l10n/en.json +6 -0
  128. package/l10n/nl.json +7 -1
  129. package/package.json +1 -1
  130. package/src/components/CnActionDialog/CnActionDialog.vue +28 -3
  131. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  132. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  133. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  134. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  135. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  136. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  137. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  138. package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
  139. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  140. package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
  141. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +43 -4
  142. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  143. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  144. package/src/components/CnIndexPage/CnIndexPage.vue +18 -2
  145. package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
  146. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
  147. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  148. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  149. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
  150. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  151. package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
  152. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  153. package/src/css/dialog.css +25 -1
  154. package/src/css/kpi-card.css +3 -0
  155. package/src/css/look-board-detail.css +348 -0
  156. package/src/css/look-board-index.css +56 -1
  157. package/src/css/look-board.css +98 -1
  158. package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
  159. package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
  160. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
  161. package/src/integrations/builtin/calendar/eventStatus.js +63 -0
  162. package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
  163. package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
  164. package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
  165. package/src/integrations/builtin/deck/deckCardParts.js +56 -0
  166. package/src/utils/fetchFilterCounts.js +5 -2
  167. package/src/utils/nextcloudDirectory.js +19 -6
@@ -1 +1 @@
1
- {"version":3,"file":"CnAiInput.vue2.js","sources":["../../../../src/components/CnAiCompanion/CnAiInput.vue"],"sourcesContent":["<!--\n CnAiInput — multi-line textarea + send button for the AI Chat Companion.\n\n - Auto-grows vertically up to ~6 lines, then scrolls internally.\n - Enter sends and clears; Shift+Enter inserts a newline.\n - Disabled prop disables both controls; send button shows NcLoadingIcon.\n - Send button disabled when the textarea is empty/whitespace-only AND no\n attachment has been added (an attachment-only send is allowed).\n - Attach button (paperclip) is a menu: Upload from device opens a hidden\n file input; Choose from Files opens the Nextcloud file picker and adds the\n chosen files as chips carrying `{ fileId, path, name }`, sent by id with no\n upload. On the device path the picked file is\n uploaded immediately as multipart/form-data (field `file`) to the chat\n backend's attachments endpoint (see composables/aiChatConfig.js\n `attachmentsUrl()`), which text-decodes and stores it, returning\n `{ path, name }`. Successful uploads render as removable chips above the\n textarea; a 400 rejection (oversized or non-UTF-8 binary) surfaces the\n backend's `{ error }` message inline via NcNoteCard rather than being\n dropped silently.\n - On send, the accumulated attachment refs are emitted alongside the text\n as `{ text, attachments }` and cleared (mirrors the existing optimistic\n textarea clear — this component fires the `send` event and does not wait\n for the parent's async request to resolve before clearing either).\n-->\n<template>\n\t<div class=\"cn-ai-input\" data-testid=\"cn-ai-input\">\n\t\t<NcNoteCard\n\t\t\tv-if=\"uploadError\"\n\t\t\ttype=\"error\"\n\t\t\tclass=\"cn-ai-input__error\"\n\t\t\tdata-testid=\"cn-ai-input-error\">\n\t\t\t{{ uploadError }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-if=\"dictationError\"\n\t\t\ttype=\"warning\"\n\t\t\tclass=\"cn-ai-input__error\"\n\t\t\tdata-testid=\"cn-ai-input-dictation-error\">\n\t\t\t{{ dictationError }}\n\t\t</NcNoteCard>\n\t\t<ul\n\t\t\tv-if=\"attachments.length\"\n\t\t\tclass=\"cn-ai-input__chips\"\n\t\t\tdata-testid=\"cn-ai-input-chips\">\n\t\t\t<li\n\t\t\t\tv-for=\"(attachment, index) in attachments\"\n\t\t\t\t:key=\"attachment.fileId !== undefined ? 'f' + attachment.fileId : attachment.path\"\n\t\t\t\tclass=\"cn-ai-input__chip\">\n\t\t\t\t<Paperclip :size=\"14\" class=\"cn-ai-input__chip-icon\" />\n\t\t\t\t<span class=\"cn-ai-input__chip-name\">{{ attachment.name }}</span>\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-ai-input__chip-remove\"\n\t\t\t\t\t:aria-label=\"cnTranslate('Remove attachment')\"\n\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t:data-testid=\"'cn-ai-input-chip-remove-' + index\"\n\t\t\t\t\t@click=\"removeAttachment(index)\">\n\t\t\t\t\t<Close :size=\"14\" />\n\t\t\t\t</button>\n\t\t\t</li>\n\t\t</ul>\n\t\t<div class=\"cn-ai-input__row\">\n\t\t\t<!--\n\t\t\t Dictation sits LEFT of the paperclip: both are ways of getting content\n\t\t\t into the message, and speech is the one reached for first when typing\n\t\t\t is the obstacle.\n\n\t\t\t Rendered only where the browser can actually do it. A mic button that\n\t\t\t silently does nothing is worse than no mic button, and support is\n\t\t\t genuinely partial (WebKit-prefixed in Safari, absent in Firefox).\n\t\t\t-->\n\t\t\t<!--\n\t\t\t ⚠️ NOT the `disabled` attribute when dictation is merely blocked.\n\t\t\t Browsers suppress hover events on a disabled control, so its\n\t\t\t `title` tooltip can never appear — the button would look dead and\n\t\t\t still refuse to say why, which is the bug this is fixing.\n\n\t\t\t So: `aria-disabled` for assistive tech, a class for the muted look,\n\t\t\t and a live click handler that puts the reason on screen. The real\n\t\t\t `disabled` attribute is still used for the one case that is not a\n\t\t\t capability problem — a turn already streaming.\n\t\t\t-->\n\t\t\t<button\n\t\t\t\tv-if=\"speechButtonVisible\"\n\t\t\t\tclass=\"cn-ai-input__mic-button\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-ai-input__mic-button--recording': micIsOpen,\n\t\t\t\t\t'cn-ai-input__mic-button--transcribing': transcribing,\n\t\t\t\t\t'cn-ai-input__mic-button--blocked': speechBlockedReason !== '',\n\t\t\t\t}\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"micButtonLabel\"\n\t\t\t\t:title=\"micButtonLabel\"\n\t\t\t\t:aria-pressed=\"micIsOpen ? 'true' : 'false'\"\n\t\t\t\t:aria-disabled=\"speechBlockedReason !== '' ? 'true' : 'false'\"\n\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t:data-engine=\"dictationDecision.engine\"\n\t\t\t\tdata-testid=\"cn-ai-input-mic\"\n\t\t\t\t@click=\"toggleDictation\">\n\t\t\t\t<!--\n\t\t\t\t 🔴 THE FILLED MIC MEANS LIVE. THIS WAS THE OTHER WAY AROUND.\n\t\t\t\t A struck-through mic (`MicrophoneOff`) rendered WHILE\n\t\t\t\t RECORDING, on the reasoning that a button should picture the\n\t\t\t\t action its click performs. Every other product on the user's\n\t\t\t\t machine uses that glyph for \"muted\", so the composer read as\n\t\t\t\t off while it was listening and on while it was idle —\n\t\t\t\t reported from a live session where the reporter was watching\n\t\t\t\t their own words appear under an icon saying the mic was off.\n\n\t\t\t\t A control's icon states what IS, not what clicking does; the\n\t\t\t\t label and `aria-pressed` carry the action.\n\n\t\t\t\t Outline vs filled rather than a colour change alone, so the\n\t\t\t\t two states are distinguishable without perceiving the red\n\t\t\t\t (WCAG 2.2 SC 1.4.1 Use of Colour).\n\t\t\t\t-->\n\t\t\t\t<!-- The clip is recorded; this is the wait for the transcript.\n\t\t\t\t The microphone is already closed, so showing the recording\n\t\t\t\t state through it would be the same lie in a new place. -->\n\t\t\t\t<NcLoadingIcon\n\t\t\t\t\tv-if=\"transcribing\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<MicrophoneIcon\n\t\t\t\t\tv-else-if=\"micIsOpen\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<MicrophoneOutline\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t\t<!--\n\t\t\t Conversation — a SEPARATE control from the microphone, offered only\n\t\t\t where the agent allows it. Same reason it is a separate icon:\n\t\t\t pressing \"dictate\" must never post a message by itself, and\n\t\t\t pressing \"converse\" is the act of agreeing that it will.\n\t\t\t-->\n\t\t\t<button\n\t\t\t\tv-if=\"conversationButtonVisible\"\n\t\t\t\tclass=\"cn-ai-input__converse-button\"\n\t\t\t\t:class=\"{ 'cn-ai-input__converse-button--active': conversing }\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"converseButtonLabel\"\n\t\t\t\t:title=\"converseButtonLabel\"\n\t\t\t\t:aria-pressed=\"conversing ? 'true' : 'false'\"\n\t\t\t\t:disabled=\"disabled && conversing === false\"\n\t\t\t\tdata-testid=\"cn-ai-input-converse\"\n\t\t\t\t@click=\"toggleConversation\">\n\t\t\t\t<Headset\n\t\t\t\t\tv-if=\"conversing\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<HeadsetOff\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t\t<!-- The paperclip is a small menu: upload from the device (the\n\t\t\t hidden file input below) or choose files already in Files. -->\n\t\t\t<NcActions\n\t\t\t\tclass=\"cn-ai-input__attach-menu\"\n\t\t\t\t:aria-label=\"cnTranslate('Attach file')\"\n\t\t\t\t:disabled=\"disabled || uploading\"\n\t\t\t\t:forceMenu=\"true\"\n\t\t\t\tdata-testid=\"cn-ai-input-attach\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"uploading\" :size=\"20\" />\n\t\t\t\t\t<Paperclip v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t<NcActionButton\n\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\tdata-testid=\"cn-ai-input-attach-upload\"\n\t\t\t\t\t@click=\"openFilePicker\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Upload :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ cnTranslate('Upload from device') }}\n\t\t\t\t</NcActionButton>\n\t\t\t\t<NcActionButton\n\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\tdata-testid=\"cn-ai-input-attach-files\"\n\t\t\t\t\t@click=\"chooseFromFiles\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FolderOutline :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ cnTranslate('Choose from Files') }}\n\t\t\t\t</NcActionButton>\n\t\t\t</NcActions>\n\t\t\t<!-- `:ref`, not `ref` — a fully static input with a cached handler\n\t\t\t is hoisted to module scope, and a hoisted vnode's ref has no\n\t\t\t owner, which throws in production builds where Vue's guard is\n\t\t\t compiled out. See CnFilesTab for the full account. -->\n\t\t\t<input\n\t\t\t\t:ref=\"fileInputRef\"\n\t\t\t\ttype=\"file\"\n\t\t\t\tclass=\"cn-ai-input__file-input\"\n\t\t\t\tdata-testid=\"cn-ai-input-file\"\n\t\t\t\t@change=\"onFileSelected\">\n\t\t\t<textarea\n\t\t\t\tref=\"textarea\"\n\t\t\t\tv-model=\"inputText\"\n\t\t\t\tclass=\"cn-ai-input__textarea\"\n\t\t\t\t:aria-label=\"cnTranslate('Message input')\"\n\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t:placeholder=\"placeholder || cnTranslate('Message input')\"\n\t\t\t\trows=\"1\"\n\t\t\t\tdata-testid=\"cn-ai-input-textarea\"\n\t\t\t\t@keydown.enter.exact.prevent=\"handleEnter\"\n\t\t\t\t@keydown.shift.enter.exact=\"handleShiftEnter\"\n\t\t\t\t@input=\"autoGrow\" />\n\t\t\t<button\n\t\t\t\tclass=\"cn-ai-input__send-button\"\n\t\t\t\t:class=\"{ 'cn-ai-input__send-button--labelled': sendLabel }\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"sendLabel ? null : cnTranslate('Send message')\"\n\t\t\t\t:disabled=\"disabled || isSendDisabled\"\n\t\t\t\tdata-testid=\"cn-ai-input-send\"\n\t\t\t\t@click=\"handleSend\">\n\t\t\t\t<NcLoadingIcon\n\t\t\t\t\tv-if=\"disabled\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<template v-else-if=\"sendLabel\">\n\t\t\t\t\t{{ sendLabel }}\n\t\t\t\t</template>\n\t\t\t\t<Send\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'\nimport { NcActionButton, NcActions, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport FolderOutline from 'vue-material-design-icons/FolderOutline.vue'\nimport Headset from 'vue-material-design-icons/Headset.vue'\nimport HeadsetOff from 'vue-material-design-icons/HeadsetOff.vue'\nimport MicrophoneIcon from 'vue-material-design-icons/Microphone.vue'\nimport MicrophoneOutline from 'vue-material-design-icons/MicrophoneOutline.vue'\nimport Paperclip from 'vue-material-design-icons/Paperclip.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport Upload from 'vue-material-design-icons/Upload.vue'\nimport { attachmentsUrl, DEFAULT_CHAT_APP_ID, speechTranscriptionsUrl } from '../../composables/aiChatConfig.js'\nimport { createLocalDictation } from '../../composables/aiLocalDictation.js'\nimport {\n\tbrowserRecognitionUsable,\n\tbrowserRecordingUsable,\n\tresolveDictationEngine,\n\tSPEECH_AUTO,\n\tSPEECH_LOCAL,\n\tSPEECH_OFF,\n} from '../../composables/aiSpeechPolicy.js'\nimport { libraryTranslate } from '../../utils/libraryTranslate.js'\n\n/** How long a dictation failure stays on screen, in ms. */\nconst DICTATION_ERROR_TIMEOUT = 6000\n\nexport default {\n\tname: 'CnAiInput',\n\n\tcomponents: {\n\t\tNcActionButton,\n\t\tNcActions,\n\t\tFolderOutline,\n\t\tUpload,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tSend,\n\t\tPaperclip,\n\t\tClose,\n\t\tMicrophoneIcon,\n\t\tMicrophoneOutline,\n\t\tHeadset,\n\t\tHeadsetOff,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => libraryTranslate },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the input controls are disabled (e.g. while streaming).\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The textarea's placeholder, already translated; '' keeps \"Message input\". */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A label on the send button, already translated; '' draws the send icon. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Backend app id the attach control uploads files to\n\t\t * (`POST /index.php/apps/{chatAppId}/api/chat/attachments`). See\n\t\t * composables/aiChatConfig.js `attachmentsUrl()`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tchatAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_CHAT_APP_ID,\n\t\t},\n\n\t\t/**\n\t\t * How long a silence may last during dictation before the microphone is\n\t\t * released, in ms. `0` disables the timer and leaves the mic open until\n\t\t * the user stops it or the engine gives up on its own.\n\t\t *\n\t\t * Releasing the mic is ALL this does — the transcript stays in the box\n\t\t * for the user to read, edit and send. Dictation that posts by itself\n\t\t * turns a pause for thought into a sent message, and there is no\n\t\t * unsending. Conversation mode is where auto-send belongs, behind its\n\t\t * own control that the user chose to press.\n\t\t *\n\t\t * Per-agent, because the right pause length is a property of the work:\n\t\t * dictating a case note is not the same rhythm as answering a question.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tdictationSilenceTimeout: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2500,\n\t\t},\n\n\t\t/**\n\t\t * Which engine this agent's dictation may use: `auto`, `browser`,\n\t\t * `local` or `off`. Comes from the agent's `voiceInputEngine`.\n\t\t *\n\t\t * 🔴 `local` MEANS ONLY LOCAL. It is not a preference to be abandoned\n\t\t * when the instance's speech service is down — an agent is set to it\n\t\t * because its subject matter must not reach a cloud service, and the\n\t\t * browser engine IS a cloud service in Chrome, Edge and Safari alike.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspeechInputEngine: {\n\t\t\ttype: String,\n\t\t\tdefault: SPEECH_AUTO,\n\t\t},\n\n\t\t/**\n\t\t * Whether the instance's own speech service answered its capability\n\t\t * probe. Passed in rather than probed here so one panel makes one call\n\t\t * for every composer it owns.\n\t\t *\n\t\t * ⚠️ Defaults to FALSE, so an app that does not probe never offers a\n\t\t * private engine it has not confirmed. Wrong in the safe direction: the\n\t\t * cost is the browser engine where local would have worked, not audio\n\t\t * leaving an instance that thought it was private.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tlocalSpeechAvailable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether this agent offers hands-free conversation — a separate\n\t\t * control from the microphone, and deliberately so.\n\t\t *\n\t\t * Dictation and conversation are different acts. Dictating is writing\n\t\t * with your voice: the words land in the box and you decide when they\n\t\t * go. Conversing is talking to someone: your turn ends when you stop\n\t\t * speaking, and it is sent. Putting both on one button would mean every\n\t\t * dictated pause risks posting a half-finished thought, which is why\n\t\t * this is a second control the user chose to press.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tconversationEnabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['send', 'dictation-complete', 'conversation-state'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * The hidden file input, set by the template's function ref (kept\n\t\t\t * off `$refs` so the ref stays dynamic).\n\t\t\t *\n\t\t\t * @type {HTMLInputElement|null}\n\t\t\t */\n\t\t\tfileInputEl: null,\n\t\t\tinputText: '',\n\t\t\t/** Uploaded attachment refs: { path, name } — awaiting send. */\n\t\t\tattachments: [],\n\t\t\t/** Whether an upload request is in-flight. */\n\t\t\tuploading: false,\n\t\t\t/** Backend `{ error }` message from a rejected (400) upload, or ''. */\n\t\t\tuploadError: '',\n\t\t\t/** Whether dictation is currently capturing. */\n\t\t\tlistening: false,\n\t\t\t/** The live SpeechRecognition instance, or null when idle. */\n\t\t\trecognition: null,\n\t\t\t/** Text already in the box when dictation started, so it is not lost. */\n\t\t\ttextBeforeDictation: '',\n\t\t\t/** Timer that releases the mic after a silence. null when not armed. */\n\t\t\tsilenceTimer: null,\n\t\t\t/** Whether a hands-free conversation is running. */\n\t\t\tconversing: false,\n\t\t\t/** The local-engine session, or null when idle. */\n\t\t\tlocalSession: null,\n\t\t\t/** Whether the local engine is recording right now. */\n\t\t\tlocalRecording: false,\n\t\t\t/**\n\t\t\t * Whether a recorded clip is being transcribed.\n\t\t\t *\n\t\t\t * Its own state, not folded into `localRecording`: the microphone is\n\t\t\t * already closed by then, and showing \"recording\" through a\n\t\t\t * multi-second upload is the same lie the mic icon used to tell.\n\t\t\t */\n\t\t\ttranscribing: false,\n\t\t\t/** Last dictation failure, shown to the user. '' when none. */\n\t\t\tdictationError: '',\n\t\t\t/** Timer clearing that message, so it does not outlive its moment. */\n\t\t\tdictationErrorTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether this browser can do speech recognition at all.\n\t\t *\n\t\t * Checked rather than assumed: Chrome and Safari expose it (Safari only\n\t\t * under the `webkit` prefix) and Firefox does not expose it at all.\n\t\t *\n\t\t * ⚠️ This answers \"is the BROWSER engine available\", which since the\n\t\t * per-agent policy landed is no longer the same question as \"may this\n\t\t * agent dictate\" — see `dictationDecision`. Used only by the browser\n\t\t * path's own guard.\n\t\t *\n\t\t * @return {boolean} true when a SpeechRecognition constructor exists.\n\t\t */\n\t\tspeechSupported() {\n\t\t\treturn browserRecognitionUsable()\n\t\t},\n\n\t\t/**\n\t\t * Why dictation cannot run here, or '' when it can.\n\t\t *\n\t\t * 🔴 The constructor EXISTING is not the capability. On an insecure\n\t\t * origin Chrome still exposes `webkitSpeechRecognition`, so a\n\t\t * constructor check passes, and then `start()` fires `onerror:\n\t\t * not-allowed` immediately — measured on this instance over http://,\n\t\t * where `isSecureContext` is false and `navigator.mediaDevices` is\n\t\t * undefined. The first version of this button did exactly that and\n\t\t * appeared to do nothing at all when pressed.\n\t\t *\n\t\t * Reported rather than hidden: the button vanishing reads as a missing\n\t\t * feature, while a disabled button that says why reads as a\n\t\t * deployment fact the user can act on (serve over https).\n\t\t *\n\t\t * @return {string} A reason to show, or '' when dictation is available.\n\t\t */\n\t\tspeechBlockedReason() {\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\treturn this.cnTranslate(this.dictationDecision.reason)\n\t\t\t}\n\t\t\tif (typeof window !== 'undefined' && window.isSecureContext === false) {\n\t\t\t\treturn this.cnTranslate('Dictation needs a secure (https) connection')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Which engine this agent's dictation may use, and why not when it may not.\n\t\t *\n\t\t * The DECISION, not a preference — `startDictation` runs exactly what\n\t\t * this names. See composables/aiSpeechPolicy.js for the rule that matters:\n\t\t * an agent pinned to the private engine never falls back to the browser's,\n\t\t * because that fallback would send confidential audio to a cloud service.\n\t\t *\n\t\t * @return {{engine: string, reason: string}} The decision.\n\t\t */\n\t\tdictationDecision() {\n\t\t\treturn resolveDictationEngine(\n\t\t\t\t{ inputEngine: this.speechInputEngine },\n\t\t\t\t{\n\t\t\t\t\tbrowserUsable: browserRecognitionUsable(),\n\t\t\t\t\tlocalUsable: this.localSpeechAvailable === true && browserRecordingUsable(),\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the mic button is offered at all.\n\t\t *\n\t\t * ⚠️ HIDDEN ONLY WHEN THE AGENT SAYS SO. An unavailable engine still\n\t\t * renders the button, disabled and carrying its reason: a control that\n\t\t * vanishes reads as a missing feature, while one that says \"this agent\n\t\t * may only use the private speech service, and it is unavailable\" reads\n\t\t * as a fact somebody can act on. The exception is `off`, where the agent\n\t\t * has decided there is no dictation — then there is nothing to explain.\n\t\t *\n\t\t * @return {boolean} true when the button should render.\n\t\t */\n\t\tspeechButtonVisible() {\n\t\t\tif (this.speechInputEngine === SPEECH_OFF) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.dictationDecision.engine !== SPEECH_OFF\n\t\t\t\t|| this.dictationDecision.reason !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether dictation is currently running on either engine.\n\t\t *\n\t\t * @return {boolean} true while the microphone is open.\n\t\t */\n\t\tmicIsOpen() {\n\t\t\treturn this.listening === true || this.localRecording === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the conversation control is offered.\n\t\t *\n\t\t * Both conditions matter: the agent has to allow it, AND there has to be\n\t\t * an engine that can hear you. A hands-free control on an agent with no\n\t\t * working microphone is a button that can only disappoint.\n\t\t *\n\t\t * @return {boolean} true when the control should render.\n\t\t */\n\t\tconversationButtonVisible() {\n\t\t\treturn this.conversationEnabled === true\n\t\t\t\t&& this.dictationDecision.engine !== SPEECH_OFF\n\t\t},\n\n\t\t/**\n\t\t * The conversation control's label.\n\t\t *\n\t\t * Says what pressing it will DO, including the part people need warning\n\t\t * about — that a pause sends the message.\n\t\t *\n\t\t * @return {string} The label.\n\t\t */\n\t\tconverseButtonLabel() {\n\t\t\treturn this.conversing === true\n\t\t\t\t? this.cnTranslate('End the spoken conversation')\n\t\t\t\t: this.cnTranslate('Talk to the agent — your turn is sent when you stop speaking')\n\t\t},\n\n\t\t/**\n\t\t * The mic button's label and tooltip.\n\t\t *\n\t\t * Names the ENGINE when dictation is idle and the private one would be\n\t\t * used, because \"which service is about to hear me\" is not something a\n\t\t * user can discover any other way — and the difference between the two\n\t\t * is whether their words leave the building.\n\t\t *\n\t\t * @return {string} The label.\n\t\t */\n\t\tmicButtonLabel() {\n\t\t\tif (this.speechBlockedReason !== '') {\n\t\t\t\treturn this.speechBlockedReason\n\t\t\t}\n\t\t\tif (this.transcribing === true) {\n\t\t\t\treturn this.cnTranslate('Transcribing…')\n\t\t\t}\n\t\t\tif (this.micIsOpen === true) {\n\t\t\t\treturn this.cnTranslate('Stop dictation')\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\treturn this.cnTranslate('Dictate message (private, on this instance)')\n\t\t\t}\n\t\t\treturn this.cnTranslate('Dictate message')\n\t\t},\n\n\t\tisTextEmpty() {\n\t\t\treturn !this.inputText || !this.inputText.trim()\n\t\t},\n\n\t\t/** Send is only blocked when there's neither text nor an attachment. */\n\t\tisSendDisabled() {\n\t\t\treturn this.isTextEmpty && this.attachments.length === 0\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\t// A recogniser outliving its component keeps the microphone open — the\n\t\t// indicator stays lit in the browser chrome with nothing on screen\n\t\t// explaining why.\n\t\tthis.conversing = false\n\t\tthis.stopDictation()\n\t\tif (this.dictationErrorTimer !== null) {\n\t\t\tclearTimeout(this.dictationErrorTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Function ref for the hidden file input; a method so the binding is\n\t\t * stable across renders.\n\t\t *\n\t\t * @param {HTMLInputElement|null} el The element, or null on unmount.\n\t\t * @return {void}\n\t\t */\n\t\tfileInputRef(el) {\n\t\t\tthis.fileInputEl = el || null\n\t\t},\n\n\t\t/**\n\t\t * Show a dictation failure, and take it away again.\n\t\t *\n\t\t * A banner with no lifetime is a banner that becomes furniture: the\n\t\t * first version stayed on screen for the rest of the session, sitting\n\t\t * above the composer long after the moment it described, so it read as a\n\t\t * permanent state of the chat rather than as the result of a click.\n\t\t *\n\t\t * @param {string} message What went wrong.\n\t\t * @return {void}\n\t\t */\n\t\tshowDictationError(message) {\n\t\t\tthis.dictationError = message\n\t\t\tif (this.dictationErrorTimer !== null) {\n\t\t\t\tclearTimeout(this.dictationErrorTimer)\n\t\t\t}\n\t\t\tthis.dictationErrorTimer = setTimeout(() => {\n\t\t\t\tthis.dictationError = ''\n\t\t\t\tthis.dictationErrorTimer = null\n\t\t\t}, DICTATION_ERROR_TIMEOUT)\n\t\t},\n\n\t\t/**\n\t\t * (Re)start the silence countdown that releases the microphone.\n\t\t *\n\t\t * Called on every result — interim ones included, so a long sentence\n\t\t * keeps the mic open while the words are still arriving rather than\n\t\t * being cut off between clauses.\n\t\t *\n\t\t * ⚠️ ARMED BY SPEECH, NOT BY `start()`. Arming it when dictation begins\n\t\t * closes the microphone on somebody who pressed the button and then\n\t\t * spent four seconds deciding what to say — the exact moment the\n\t\t * feature exists to serve. Until the first result arrives the engine's\n\t\t * own `no-speech` error is the backstop, and it says something useful.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tarmSilenceTimer() {\n\t\t\tthis.clearSilenceTimer()\n\t\t\tif (this.dictationSilenceTimeout <= 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.silenceTimer = setTimeout(() => {\n\t\t\t\tthis.silenceTimer = null\n\t\t\t\t// Only the microphone is released. The text stays put.\n\t\t\t\tthis.stopDictation()\n\t\t\t\t// …unless this silence is the end of a conversational turn, which\n\t\t\t\t// is the ONE case where a pause sends — and the user pressed a\n\t\t\t\t// separate control to say so.\n\t\t\t\tthis.completeConversationTurn()\n\t\t\t}, this.dictationSilenceTimeout)\n\t\t},\n\n\t\t/**\n\t\t * Cancel a pending silence countdown.\n\t\t *\n\t\t * ⚠️ A LEFTOVER TIMER OUTLIVES THE DICTATION THAT ARMED IT and would\n\t\t * close the NEXT one mid-sentence, seconds after it started, with no\n\t\t * silence involved. So this is called from every path that ends a\n\t\t * dictation — stop, `onend`, `onerror`, unmount — not only the tidy one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearSilenceTimer() {\n\t\t\tif (this.silenceTimer !== null) {\n\t\t\t\tclearTimeout(this.silenceTimer)\n\t\t\t\tthis.silenceTimer = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start dictation, or stop it if already running.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleDictation() {\n\t\t\t// The reason is shown rather than the press being ignored: a button\n\t\t\t// that does nothing teaches nothing, and this one has something\n\t\t\t// specific to say — that the agent forbids the fast engine and the\n\t\t\t// private one is down.\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.showDictationError(this.cnTranslate(this.dictationDecision.reason))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.micIsOpen) {\n\t\t\t\tthis.stopDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.startLocalDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.startDictation()\n\t\t},\n\n\t\t/**\n\t\t * Start or end a hands-free conversation.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleConversation() {\n\t\t\tif (this.conversing === true) {\n\t\t\t\tthis.endConversation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.showDictationError(this.cnTranslate(this.dictationDecision.reason))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.conversing = true\n\t\t\tthis.$emit('conversation-state', true)\n\t\t\tthis.beginConversationTurn()\n\t\t},\n\n\t\t/**\n\t\t * Stop conversing and release the microphone.\n\t\t *\n\t\t * ⚠️ The FIRST thing it does is clear the flag, before stopping\n\t\t * dictation. `stopDictation` runs the same paths a silence does, and\n\t\t * with the flag still set the engine's own stop event would be read as\n\t\t * \"your turn ended\" and send whatever was captured — so pressing \"end\n\t\t * conversation\" would post one last message on the way out.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tendConversation() {\n\t\t\tthis.conversing = false\n\t\t\tthis.$emit('conversation-state', false)\n\t\t\tthis.stopDictation()\n\t\t},\n\n\t\t/**\n\t\t * Open the microphone for the user's next turn.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tbeginConversationTurn() {\n\t\t\tif (this.conversing === false || this.disabled === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.startLocalDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.startDictation()\n\t\t},\n\n\t\t/**\n\t\t * Called by the panel once the agent has finished answering — and, when\n\t\t * the reply is spoken aloud, once it has finished being spoken.\n\t\t *\n\t\t * 🔴 THE MICROPHONE MUST NOT REOPEN WHILE THE AGENT IS TALKING. It would\n\t\t * record the reply through the speakers and hand it back as the user's\n\t\t * next turn, and with auto-send on a silence that becomes a conversation\n\t\t * the agent is having with itself. Which is why the resume is driven\n\t\t * from outside rather than from a timer in here: only the side that owns\n\t\t * the speaking knows when it stopped.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresumeConversation() {\n\t\t\tthis.beginConversationTurn()\n\t\t},\n\n\t\t/**\n\t\t * End the user's turn: send what was heard.\n\t\t *\n\t\t * Nothing is sent when nothing was heard — an empty turn means the\n\t\t * microphone opened, caught silence and closed, which should leave the\n\t\t * conversation waiting rather than post a blank message.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcompleteConversationTurn() {\n\t\t\tif (this.conversing === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isSendDisabled === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.handleSend()\n\t\t},\n\n\t\t/**\n\t\t * Dictate through the instance's own speech service.\n\t\t *\n\t\t * No partial text on the way — whisper answers a finished clip — so the\n\t\t * feedback is the recording state and then an explicit \"transcribing\"\n\t\t * one. The result is APPENDED to whatever is already in the box, the\n\t\t * same as the browser path, so dictating twice builds a message rather\n\t\t * than replacing it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartLocalDictation() {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.dictationError = ''\n\t\t\tthis.localSession = createLocalDictation({\n\t\t\t\tsilenceTimeout: this.dictationSilenceTimeout,\n\t\t\t\ttranscribe: (blob) => this.uploadForTranscription(blob),\n\t\t\t\tonTranscript: (text) => {\n\t\t\t\t\tconst existing = this.inputText.trim()\n\t\t\t\t\tthis.inputText = (existing === '') ? text : `${existing} ${text}`\n\t\t\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t\t\t\tthis.$emit('dictation-complete', text)\n\t\t\t\t\t// The local engine has no silence event of its own to hang\n\t\t\t\t\t// this on — the transcript ARRIVING is the end of the turn,\n\t\t\t\t\t// seconds after the speaker actually stopped.\n\t\t\t\t\tthis.completeConversationTurn()\n\t\t\t\t},\n\t\t\t\tonError: (message) => {\n\t\t\t\t\tthis.showDictationError(this.cnTranslate(message))\n\t\t\t\t},\n\t\t\t\tonStateChange: (state) => {\n\t\t\t\t\tthis.localRecording = (state === 'recording')\n\t\t\t\t\tthis.transcribing = (state === 'transcribing')\n\t\t\t\t},\n\t\t\t})\n\t\t\tthis.localSession.start()\n\t\t},\n\n\t\t/**\n\t\t * Send a recorded clip to the backend for transcription.\n\t\t *\n\t\t * @param {Blob} blob The recorded audio.\n\t\t * @return {Promise<{text: string}>} The transcript.\n\t\t */\n\t\tasync uploadForTranscription(blob) {\n\t\t\tconst formData = new FormData()\n\t\t\tformData.append('audio', blob, 'dictation.webm')\n\t\t\t// The page's language, for the same reason the browser engine is\n\t\t\t// given it: auto-detection misfires on short utterances, and a\n\t\t\t// Dutch sentence detected as English comes back as nonsense rather\n\t\t\t// than as an error.\n\t\t\tif (typeof document !== 'undefined' && document.documentElement.lang) {\n\t\t\t\tformData.append('language', document.documentElement.lang)\n\t\t\t}\n\t\t\tconst response = await axios.post(speechTranscriptionsUrl(this.chatAppId), formData)\n\n\t\t\treturn (response && response.data) || { text: '' }\n\t\t},\n\n\t\t/**\n\t\t * Begin capturing speech into the message box.\n\t\t *\n\t\t * Interim results are shown as they arrive so the user can see it is\n\t\t * working — dictation with no visible feedback reads as broken. Whatever\n\t\t * was already typed is preserved and appended to, rather than replaced.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartDictation() {\n\t\t\tif (!this.speechSupported || this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.dictationError = ''\n\t\t\tconst Recognition = window.SpeechRecognition || window.webkitSpeechRecognition\n\t\t\tconst recognition = new Recognition()\n\t\t\trecognition.continuous = true\n\t\t\trecognition.interimResults = true\n\t\t\t// Follow the page's language rather than hardcoding one: this ships to\n\t\t\t// Dutch and English instances alike.\n\t\t\trecognition.lang = (typeof document !== 'undefined' && document.documentElement.lang)\n\t\t\t\t? document.documentElement.lang\n\t\t\t\t: 'en-US'\n\n\t\t\tthis.textBeforeDictation = this.inputText\n\n\t\t\trecognition.onresult = (event) => {\n\t\t\t\tlet finalText = ''\n\t\t\t\tlet interimText = ''\n\t\t\t\tfor (let i = event.resultIndex; i < event.results.length; i++) {\n\t\t\t\t\tconst result = event.results[i]\n\t\t\t\t\tif (result.isFinal) {\n\t\t\t\t\t\tfinalText += result[0].transcript\n\t\t\t\t\t} else {\n\t\t\t\t\t\tinterimText += result[0].transcript\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (finalText !== '') {\n\t\t\t\t\tthis.textBeforeDictation = `${this.textBeforeDictation}${finalText}`.replace(/\\s+/g, ' ')\n\t\t\t\t}\n\t\t\t\tthis.inputText = `${this.textBeforeDictation}${interimText}`.trimStart()\n\t\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t\t\tthis.armSilenceTimer()\n\t\t\t}\n\n\t\t\t// `onend` fires for a user stop AND for the engine giving up on\n\t\t\t// silence, so the button state is driven from here rather than from\n\t\t\t// the click — otherwise the button lies after an idle timeout.\n\t\t\trecognition.onend = () => {\n\t\t\t\tthis.clearSilenceTimer()\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\n\t\t\t// SHOW the failure. Silently resetting `listening` is what made the\n\t\t\t// button look inert: the user pressed it, nothing changed, and there\n\t\t\t// was nowhere to find out why. `not-allowed` is the common one — an\n\t\t\t// insecure origin or a denied microphone permission.\n\t\t\trecognition.onerror = (event) => {\n\t\t\t\tconst code = (event && event.error) ? String(event.error) : 'unknown'\n\t\t\t\tthis.showDictationError((code === 'not-allowed')\n\t\t\t\t\t? this.cnTranslate('Microphone blocked — allow access, or use https')\n\t\t\t\t\t: this.cnTranslate('Dictation stopped: ') + code)\n\t\t\t\tthis.clearSilenceTimer()\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\trecognition.start()\n\t\t\t\tthis.recognition = recognition\n\t\t\t\tthis.listening = true\n\t\t\t} catch (e) {\n\t\t\t\t// `start()` throws if called twice, and on some browsers when the\n\t\t\t\t// origin is not permitted. Either way say so rather than resetting\n\t\t\t\t// in silence.\n\t\t\t\tthis.showDictationError(this.cnTranslate('Dictation could not start: ') + (e.message || 'unknown'))\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Stop dictation and release the microphone.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopDictation() {\n\t\t\tthis.clearSilenceTimer()\n\t\t\tif (this.localSession !== null) {\n\t\t\t\t// The local session owns its own teardown, including releasing\n\t\t\t\t// the microphone BEFORE the transcript request goes out.\n\t\t\t\tthis.localSession.stop()\n\t\t\t\tthis.localSession = null\n\t\t\t\tthis.localRecording = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.recognition) {\n\t\t\t\tthis.listening = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.recognition.stop()\n\t\t\t} catch {\n\t\t\t\t// Already stopped; nothing to release.\n\t\t\t}\n\t\t\tthis.recognition = null\n\t\t\tthis.listening = false\n\t\t},\n\n\t\thandleEnter() {\n\t\t\tif (this.disabled || this.isSendDisabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.handleSend()\n\t\t},\n\n\t\thandleShiftEnter() {\n\t\t\t// Default browser behaviour inserts a newline — we don't need to prevent it.\n\t\t\t// But we do need to trigger autoGrow after the character is inserted.\n\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t},\n\n\t\thandleSend() {\n\t\t\tif (this.disabled || this.isSendDisabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst text = this.inputText.trim()\n\t\t\tconst attachments = this.attachments.slice()\n\t\t\tthis.inputText = ''\n\t\t\tthis.attachments = []\n\t\t\tthis.uploadError = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.autoGrow()\n\t\t\t})\n\t\t\tthis.$emit('send', { text, attachments })\n\t\t},\n\n\t\tautoGrow() {\n\t\t\tconst el = this.$refs.textarea\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Reset height first so shrinking works\n\t\t\tel.style.height = 'auto'\n\t\t\t// Clamp to 6-line max (~1.5em per line + padding)\n\t\t\tconst maxHeight = parseInt(getComputedStyle(el).lineHeight || '24', 10) * 6 + 24\n\t\t\tel.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'\n\t\t},\n\n\t\t/** Focus the textarea — called by the parent panel on open. */\n\t\tfocus() {\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.textarea) {\n\t\t\t\t\tthis.$refs.textarea.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** Open the hidden file input in response to the paperclip button. */\n\t\topenFilePicker() {\n\t\t\tif (this.disabled || this.uploading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploadError = ''\n\t\t\tif (this.fileInputEl) {\n\t\t\t\tthis.fileInputEl.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Upload the picked file to the chat backend's attachments endpoint and,\n\t\t * on success, add it as a removable chip. Surfaces the backend's\n\t\t * `{ error }` message inline on a 400 rejection (oversized, or not\n\t\t * text-decodable) instead of silently dropping the file.\n\t\t *\n\t\t * @param {Event} event The file input's change event.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onFileSelected(event) {\n\t\t\tconst file = event.target && event.target.files && event.target.files[0]\n\t\t\tevent.target.value = ''\n\t\t\tif (!file) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploading = true\n\t\t\tthis.uploadError = ''\n\t\t\ttry {\n\t\t\t\tconst formData = new FormData()\n\t\t\t\tformData.append('file', file)\n\t\t\t\tconst response = await axios.post(attachmentsUrl(this.chatAppId), formData)\n\t\t\t\tconst { path, name } = response.data || {}\n\t\t\t\tthis.attachments.push({ path, name: name || file.name })\n\t\t\t} catch (err) {\n\t\t\t\tconst backendMessage = err && err.response && err.response.status === 400\n\t\t\t\t\t? err.response.data && err.response.data.error\n\t\t\t\t\t: null\n\t\t\t\tthis.uploadError = backendMessage || this.cnTranslate('Could not attach file')\n\t\t\t} finally {\n\t\t\t\tthis.uploading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pick files already in Nextcloud Files and add each as a chip carrying\n\t\t * its file id; they are sent by id and never uploaded. A dismissed\n\t\t * picker (which rejects in some dialog versions) is a no-op.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync chooseFromFiles() {\n\t\t\tif (this.disabled || this.uploading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploadError = ''\n\t\t\ttry {\n\t\t\t\tconst picker = getFilePickerBuilder(this.cnTranslate('Choose from Files'))\n\t\t\t\t\t.setMultiSelect(true)\n\t\t\t\t\t.setType(FilePickerType.Choose)\n\t\t\t\t\t.allowDirectories(false)\n\t\t\t\t\t.setModal(true)\n\t\t\t\t\t.build()\n\t\t\t\tconst nodes = await picker.pickNodes()\n\t\t\t\tfor (const node of (Array.isArray(nodes) ? nodes : [])) {\n\t\t\t\t\tconst fileId = node.fileid ?? node.attributes?.fileid\n\t\t\t\t\tconst path = node.path || ''\n\t\t\t\t\tif (fileId === null || fileId === undefined || this.attachments.some((a) => a.fileId === fileId)) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tthis.attachments.push({ fileId, path, name: node.basename || path.split('/').pop() })\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.debug('[CnAiInput] file picker closed', e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove a pending attachment chip before it's sent.\n\t\t *\n\t\t * @param {number} index Index into `attachments`.\n\t\t */\n\t\tremoveAttachment(index) {\n\t\t\tthis.attachments.splice(index, 1)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-input {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-ai-input__error {\n\tmargin: 0;\n}\n\n.cn-ai-input__chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 6px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-ai-input__chip {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tmax-width: 220px;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius-pill, 16px);\n\tbackground: var(--color-background-hover);\n\tfont-size: 13px;\n}\n\n.cn-ai-input__chip-icon {\n\tflex-shrink: 0;\n\topacity: 0.7;\n}\n\n.cn-ai-input__chip-name {\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n\n.cn-ai-input__chip-remove {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 0;\n\tborder: none;\n\tbackground: transparent;\n\tcolor: inherit;\n\tcursor: pointer;\n\topacity: 0.7;\n}\n\n/* `:focus-visible` and `:disabled` are declared BEFORE the compound\n `:hover:not(:disabled)` rule: stylelint's no-descending-specificity wants\n lower-specificity selectors first, and `:hover:not(:disabled)` outranks\n both. Ordering only — the cascade result is unchanged. */\n.cn-ai-input__chip-remove:focus-visible {\n\topacity: 1;\n}\n\n.cn-ai-input__chip-remove:disabled {\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__chip-remove:hover:not(:disabled) {\n\topacity: 1;\n}\n\n.cn-ai-input__row {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: flex-end;\n}\n\n.cn-ai-input__file-input {\n\tdisplay: none;\n}\n\n/* Mic matches the paperclip exactly — they are peers in the row, and any\n difference in size or weight would read as one being the primary action. */\n.cn-ai-input__mic-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n/* Declared BEFORE the hover rule below, which is more specific\n (`:hover:not(:disabled)`). Source order has to follow specificity here or the\n cascade's outcome depends on which rule happens to be written last — the\n thing `no-descending-specificity` exists to stop. */\n.cn-ai-input__mic-button:disabled {\n\topacity: .5;\n\tcursor: default;\n}\n\n.cn-ai-input__mic-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* Recording is a STATE, not a hover: it stays visible while the user speaks and\n looks away from the button. Error colour because it is the \"something is\n live\" signal, and the icon fills in beside it — it does NOT swap to a\n struck-through mic, which is what this comment used to describe and what made\n a listening composer look muted. */\n.cn-ai-input__mic-button--recording {\n\tborder-color: var(--color-error);\n\tbackground: var(--color-error);\n\tcolor: var(--color-primary-text, #fff);\n}\n\n/* 🔴 THE BLOCKED LOOK THE COMMENT ABOVE THE BUTTON ALREADY PROMISED.\n `cn-ai-input__mic-button--blocked` was bound in the template and styled\n NOWHERE, so a microphone that cannot run — an agent pinned to the private\n engine while the speech service is down, or a browser with no recognition —\n looked exactly like a working one until you hovered it for the tooltip. A\n class set and never read is a comment, not a state.\n\n ⚠️ Muted rather than `disabled`, and NOT `pointer-events: none`. The button\n deliberately keeps a live click handler: a natively disabled control\n suppresses hover, so its `title` can never appear and it refuses without ever\n saying why. It must stay clickable in order to explain itself. */\n.cn-ai-input__mic-button--blocked {\n\topacity: .5;\n\tcursor: not-allowed;\n}\n\n/* The conversation control sits beside the mic and matches it in weight —\n neither is the primary action, and a bigger one would suggest the hands-free\n mode is the normal way to use the composer rather than a deliberate choice. */\n.cn-ai-input__converse-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__converse-button:disabled {\n\topacity: .5;\n\tcursor: default;\n}\n\n.cn-ai-input__converse-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* Primary rather than error colour: a live conversation is a mode the user is\n in, not a recording warning — the microphone beside it carries that. */\n.cn-ai-input__converse-button--active {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n}\n\n.cn-ai-input__attach-menu {\n\tflex: 0 0 auto;\n}\n\n.cn-ai-input__attach-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__attach-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__attach-button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__attach-button:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__textarea {\n\tflex: 1;\n\tmin-height: 36px;\n\tmax-height: calc(6 * 1.5em + 24px);\n\tpadding: 8px;\n\tresize: none;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tline-height: 1.5;\n\toverflow-y: auto;\n}\n\n.cn-ai-input__textarea:focus {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 0;\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-ai-input__textarea:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__send-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: none;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__send-button--labelled {\n\twidth: auto;\n\tmin-height: 40px;\n\tpadding: 0 14px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n}\n\n.cn-ai-input__send-button:hover:not(:disabled) {\n\tbackground: var(--color-primary-element-hover, var(--color-primary-element));\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__send-button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__send-button:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["axios"],"mappings":"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"}
1
+ {"version":3,"file":"CnAiInput.vue2.js","sources":["../../../../src/components/CnAiCompanion/CnAiInput.vue"],"sourcesContent":["<!--\n CnAiInput — multi-line textarea + send button for the AI Chat Companion.\n\n - Auto-grows vertically up to ~6 lines, then scrolls internally.\n - Enter sends and clears; Shift+Enter inserts a newline.\n - Disabled prop disables both controls; send button shows NcLoadingIcon.\n - Send button disabled when the textarea is empty/whitespace-only AND no\n attachment has been added (an attachment-only send is allowed).\n - Attach button (paperclip) is a menu: Upload from device opens a hidden\n file input; Choose from Files opens the Nextcloud file picker and adds the\n chosen files as chips carrying `{ fileId, path, name }`, sent by id with no\n upload. On the device path the picked file is\n uploaded immediately as multipart/form-data (field `file`) to the chat\n backend's attachments endpoint (see composables/aiChatConfig.js\n `attachmentsUrl()`), which text-decodes and stores it, returning\n `{ path, name }`. Successful uploads render as removable chips above the\n textarea; a 400 rejection (oversized or non-UTF-8 binary) surfaces the\n backend's `{ error }` message inline via NcNoteCard rather than being\n dropped silently.\n - On send, the accumulated attachment refs are emitted alongside the text\n as `{ text, attachments }` and cleared (mirrors the existing optimistic\n textarea clear — this component fires the `send` event and does not wait\n for the parent's async request to resolve before clearing either).\n-->\n<template>\n\t<div class=\"cn-ai-input\" :class=\"{ 'cn-ai-input--stacked': stacked }\" data-testid=\"cn-ai-input\">\n\t\t<NcNoteCard\n\t\t\tv-if=\"uploadError\"\n\t\t\ttype=\"error\"\n\t\t\tclass=\"cn-ai-input__error\"\n\t\t\tdata-testid=\"cn-ai-input-error\">\n\t\t\t{{ uploadError }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-if=\"dictationError\"\n\t\t\ttype=\"warning\"\n\t\t\tclass=\"cn-ai-input__error\"\n\t\t\tdata-testid=\"cn-ai-input-dictation-error\">\n\t\t\t{{ dictationError }}\n\t\t</NcNoteCard>\n\t\t<ul\n\t\t\tv-if=\"attachments.length\"\n\t\t\tclass=\"cn-ai-input__chips\"\n\t\t\tdata-testid=\"cn-ai-input-chips\">\n\t\t\t<li\n\t\t\t\tv-for=\"(attachment, index) in attachments\"\n\t\t\t\t:key=\"attachment.fileId !== undefined ? 'f' + attachment.fileId : attachment.path\"\n\t\t\t\tclass=\"cn-ai-input__chip\">\n\t\t\t\t<Paperclip :size=\"14\" class=\"cn-ai-input__chip-icon\" />\n\t\t\t\t<span class=\"cn-ai-input__chip-name\">{{ attachment.name }}</span>\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-ai-input__chip-remove\"\n\t\t\t\t\t:aria-label=\"cnTranslate('Remove attachment')\"\n\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t:data-testid=\"'cn-ai-input-chip-remove-' + index\"\n\t\t\t\t\t@click=\"removeAttachment(index)\">\n\t\t\t\t\t<Close :size=\"14\" />\n\t\t\t\t</button>\n\t\t\t</li>\n\t\t</ul>\n\t\t<div class=\"cn-ai-input__row\">\n\t\t\t<!--\n\t\t\t Dictation sits LEFT of the paperclip: both are ways of getting content\n\t\t\t into the message, and speech is the one reached for first when typing\n\t\t\t is the obstacle.\n\n\t\t\t Rendered only where the browser can actually do it. A mic button that\n\t\t\t silently does nothing is worse than no mic button, and support is\n\t\t\t genuinely partial (WebKit-prefixed in Safari, absent in Firefox).\n\t\t\t-->\n\t\t\t<!--\n\t\t\t ⚠️ NOT the `disabled` attribute when dictation is merely blocked.\n\t\t\t Browsers suppress hover events on a disabled control, so its\n\t\t\t `title` tooltip can never appear — the button would look dead and\n\t\t\t still refuse to say why, which is the bug this is fixing.\n\n\t\t\t So: `aria-disabled` for assistive tech, a class for the muted look,\n\t\t\t and a live click handler that puts the reason on screen. The real\n\t\t\t `disabled` attribute is still used for the one case that is not a\n\t\t\t capability problem — a turn already streaming.\n\t\t\t-->\n\t\t\t<button\n\t\t\t\tv-if=\"speechButtonVisible\"\n\t\t\t\tclass=\"cn-ai-input__mic-button\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-ai-input__mic-button--recording': micIsOpen,\n\t\t\t\t\t'cn-ai-input__mic-button--transcribing': transcribing,\n\t\t\t\t\t'cn-ai-input__mic-button--blocked': speechBlockedReason !== '',\n\t\t\t\t}\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"micButtonLabel\"\n\t\t\t\t:title=\"micButtonLabel\"\n\t\t\t\t:aria-pressed=\"micIsOpen ? 'true' : 'false'\"\n\t\t\t\t:aria-disabled=\"speechBlockedReason !== '' ? 'true' : 'false'\"\n\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t:data-engine=\"dictationDecision.engine\"\n\t\t\t\tdata-testid=\"cn-ai-input-mic\"\n\t\t\t\t@click=\"toggleDictation\">\n\t\t\t\t<!--\n\t\t\t\t 🔴 THE FILLED MIC MEANS LIVE. THIS WAS THE OTHER WAY AROUND.\n\t\t\t\t A struck-through mic (`MicrophoneOff`) rendered WHILE\n\t\t\t\t RECORDING, on the reasoning that a button should picture the\n\t\t\t\t action its click performs. Every other product on the user's\n\t\t\t\t machine uses that glyph for \"muted\", so the composer read as\n\t\t\t\t off while it was listening and on while it was idle —\n\t\t\t\t reported from a live session where the reporter was watching\n\t\t\t\t their own words appear under an icon saying the mic was off.\n\n\t\t\t\t A control's icon states what IS, not what clicking does; the\n\t\t\t\t label and `aria-pressed` carry the action.\n\n\t\t\t\t Outline vs filled rather than a colour change alone, so the\n\t\t\t\t two states are distinguishable without perceiving the red\n\t\t\t\t (WCAG 2.2 SC 1.4.1 Use of Colour).\n\t\t\t\t-->\n\t\t\t\t<!-- The clip is recorded; this is the wait for the transcript.\n\t\t\t\t The microphone is already closed, so showing the recording\n\t\t\t\t state through it would be the same lie in a new place. -->\n\t\t\t\t<NcLoadingIcon\n\t\t\t\t\tv-if=\"transcribing\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<MicrophoneIcon\n\t\t\t\t\tv-else-if=\"micIsOpen\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<MicrophoneOutline\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t\t<!--\n\t\t\t Conversation — a SEPARATE control from the microphone, offered only\n\t\t\t where the agent allows it. Same reason it is a separate icon:\n\t\t\t pressing \"dictate\" must never post a message by itself, and\n\t\t\t pressing \"converse\" is the act of agreeing that it will.\n\t\t\t-->\n\t\t\t<button\n\t\t\t\tv-if=\"conversationButtonVisible\"\n\t\t\t\tclass=\"cn-ai-input__converse-button\"\n\t\t\t\t:class=\"{ 'cn-ai-input__converse-button--active': conversing }\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"converseButtonLabel\"\n\t\t\t\t:title=\"converseButtonLabel\"\n\t\t\t\t:aria-pressed=\"conversing ? 'true' : 'false'\"\n\t\t\t\t:disabled=\"disabled && conversing === false\"\n\t\t\t\tdata-testid=\"cn-ai-input-converse\"\n\t\t\t\t@click=\"toggleConversation\">\n\t\t\t\t<Headset\n\t\t\t\t\tv-if=\"conversing\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<HeadsetOff\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t\t<!-- The paperclip is a small menu: upload from the device (the\n\t\t\t hidden file input below) or choose files already in Files. -->\n\t\t\t<NcActions\n\t\t\t\tclass=\"cn-ai-input__attach-menu\"\n\t\t\t\t:aria-label=\"cnTranslate('Attach file')\"\n\t\t\t\t:disabled=\"disabled || uploading\"\n\t\t\t\t:forceMenu=\"true\"\n\t\t\t\tdata-testid=\"cn-ai-input-attach\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"uploading\" :size=\"20\" />\n\t\t\t\t\t<Paperclip v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t<NcActionButton\n\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\tdata-testid=\"cn-ai-input-attach-upload\"\n\t\t\t\t\t@click=\"openFilePicker\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Upload :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ cnTranslate('Upload from device') }}\n\t\t\t\t</NcActionButton>\n\t\t\t\t<NcActionButton\n\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\tdata-testid=\"cn-ai-input-attach-files\"\n\t\t\t\t\t@click=\"chooseFromFiles\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FolderOutline :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ cnTranslate('Choose from Files') }}\n\t\t\t\t</NcActionButton>\n\t\t\t</NcActions>\n\t\t\t<!-- `:ref`, not `ref` — a fully static input with a cached handler\n\t\t\t is hoisted to module scope, and a hoisted vnode's ref has no\n\t\t\t owner, which throws in production builds where Vue's guard is\n\t\t\t compiled out. See CnFilesTab for the full account. -->\n\t\t\t<input\n\t\t\t\t:ref=\"fileInputRef\"\n\t\t\t\ttype=\"file\"\n\t\t\t\tclass=\"cn-ai-input__file-input\"\n\t\t\t\tdata-testid=\"cn-ai-input-file\"\n\t\t\t\t@change=\"onFileSelected\">\n\t\t\t<textarea\n\t\t\t\tref=\"textarea\"\n\t\t\t\tv-model=\"inputText\"\n\t\t\t\tclass=\"cn-ai-input__textarea\"\n\t\t\t\t:aria-label=\"cnTranslate('Message input')\"\n\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t:placeholder=\"placeholder || cnTranslate('Message input')\"\n\t\t\t\trows=\"1\"\n\t\t\t\tdata-testid=\"cn-ai-input-textarea\"\n\t\t\t\t@keydown.enter.exact.prevent=\"handleEnter\"\n\t\t\t\t@keydown.shift.enter.exact=\"handleShiftEnter\"\n\t\t\t\t@input=\"autoGrow\" />\n\t\t\t<button\n\t\t\t\tclass=\"cn-ai-input__send-button\"\n\t\t\t\t:class=\"{ 'cn-ai-input__send-button--labelled': sendLabel }\"\n\t\t\t\ttype=\"button\"\n\t\t\t\t:aria-label=\"sendLabel ? null : cnTranslate('Send message')\"\n\t\t\t\t:disabled=\"disabled || isSendDisabled\"\n\t\t\t\tdata-testid=\"cn-ai-input-send\"\n\t\t\t\t@click=\"handleSend\">\n\t\t\t\t<NcLoadingIcon\n\t\t\t\t\tv-if=\"disabled\"\n\t\t\t\t\t:size=\"20\" />\n\t\t\t\t<template v-else-if=\"sendLabel\">\n\t\t\t\t\t{{ sendLabel }}\n\t\t\t\t</template>\n\t\t\t\t<Send\n\t\t\t\t\tv-else\n\t\t\t\t\t:size=\"20\" />\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'\nimport { NcActionButton, NcActions, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport FolderOutline from 'vue-material-design-icons/FolderOutline.vue'\nimport Headset from 'vue-material-design-icons/Headset.vue'\nimport HeadsetOff from 'vue-material-design-icons/HeadsetOff.vue'\nimport MicrophoneIcon from 'vue-material-design-icons/Microphone.vue'\nimport MicrophoneOutline from 'vue-material-design-icons/MicrophoneOutline.vue'\nimport Paperclip from 'vue-material-design-icons/Paperclip.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport Upload from 'vue-material-design-icons/Upload.vue'\nimport { attachmentsUrl, DEFAULT_CHAT_APP_ID, speechTranscriptionsUrl } from '../../composables/aiChatConfig.js'\nimport { createLocalDictation } from '../../composables/aiLocalDictation.js'\nimport {\n\tbrowserRecognitionUsable,\n\tbrowserRecordingUsable,\n\tresolveDictationEngine,\n\tSPEECH_AUTO,\n\tSPEECH_LOCAL,\n\tSPEECH_OFF,\n} from '../../composables/aiSpeechPolicy.js'\nimport { libraryTranslate } from '../../utils/libraryTranslate.js'\n\n/** How long a dictation failure stays on screen, in ms. */\nconst DICTATION_ERROR_TIMEOUT = 6000\n\nexport default {\n\tname: 'CnAiInput',\n\n\tcomponents: {\n\t\tNcActionButton,\n\t\tNcActions,\n\t\tFolderOutline,\n\t\tUpload,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tSend,\n\t\tPaperclip,\n\t\tClose,\n\t\tMicrophoneIcon,\n\t\tMicrophoneOutline,\n\t\tHeadset,\n\t\tHeadsetOff,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => libraryTranslate },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the input controls are disabled (e.g. while streaming).\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The textarea's placeholder, already translated; '' keeps \"Message input\". */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A label on the send button, already translated; '' draws the send icon. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The narrow layout of a card: the text box and the send button on the\n\t\t * first line, dictation and attach on a line under it.\n\t\t */\n\t\tstacked: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Backend app id the attach control uploads files to\n\t\t * (`POST /index.php/apps/{chatAppId}/api/chat/attachments`). See\n\t\t * composables/aiChatConfig.js `attachmentsUrl()`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tchatAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_CHAT_APP_ID,\n\t\t},\n\n\t\t/**\n\t\t * How long a silence may last during dictation before the microphone is\n\t\t * released, in ms. `0` disables the timer and leaves the mic open until\n\t\t * the user stops it or the engine gives up on its own.\n\t\t *\n\t\t * Releasing the mic is ALL this does — the transcript stays in the box\n\t\t * for the user to read, edit and send. Dictation that posts by itself\n\t\t * turns a pause for thought into a sent message, and there is no\n\t\t * unsending. Conversation mode is where auto-send belongs, behind its\n\t\t * own control that the user chose to press.\n\t\t *\n\t\t * Per-agent, because the right pause length is a property of the work:\n\t\t * dictating a case note is not the same rhythm as answering a question.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\tdictationSilenceTimeout: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2500,\n\t\t},\n\n\t\t/**\n\t\t * Which engine this agent's dictation may use: `auto`, `browser`,\n\t\t * `local` or `off`. Comes from the agent's `voiceInputEngine`.\n\t\t *\n\t\t * 🔴 `local` MEANS ONLY LOCAL. It is not a preference to be abandoned\n\t\t * when the instance's speech service is down — an agent is set to it\n\t\t * because its subject matter must not reach a cloud service, and the\n\t\t * browser engine IS a cloud service in Chrome, Edge and Safari alike.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspeechInputEngine: {\n\t\t\ttype: String,\n\t\t\tdefault: SPEECH_AUTO,\n\t\t},\n\n\t\t/**\n\t\t * Whether the instance's own speech service answered its capability\n\t\t * probe. Passed in rather than probed here so one panel makes one call\n\t\t * for every composer it owns.\n\t\t *\n\t\t * ⚠️ Defaults to FALSE, so an app that does not probe never offers a\n\t\t * private engine it has not confirmed. Wrong in the safe direction: the\n\t\t * cost is the browser engine where local would have worked, not audio\n\t\t * leaving an instance that thought it was private.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tlocalSpeechAvailable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether this agent offers hands-free conversation — a separate\n\t\t * control from the microphone, and deliberately so.\n\t\t *\n\t\t * Dictation and conversation are different acts. Dictating is writing\n\t\t * with your voice: the words land in the box and you decide when they\n\t\t * go. Conversing is talking to someone: your turn ends when you stop\n\t\t * speaking, and it is sent. Putting both on one button would mean every\n\t\t * dictated pause risks posting a half-finished thought, which is why\n\t\t * this is a second control the user chose to press.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tconversationEnabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['send', 'dictation-complete', 'conversation-state'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * The hidden file input, set by the template's function ref (kept\n\t\t\t * off `$refs` so the ref stays dynamic).\n\t\t\t *\n\t\t\t * @type {HTMLInputElement|null}\n\t\t\t */\n\t\t\tfileInputEl: null,\n\t\t\tinputText: '',\n\t\t\t/** Uploaded attachment refs: { path, name } — awaiting send. */\n\t\t\tattachments: [],\n\t\t\t/** Whether an upload request is in-flight. */\n\t\t\tuploading: false,\n\t\t\t/** Backend `{ error }` message from a rejected (400) upload, or ''. */\n\t\t\tuploadError: '',\n\t\t\t/** Whether dictation is currently capturing. */\n\t\t\tlistening: false,\n\t\t\t/** The live SpeechRecognition instance, or null when idle. */\n\t\t\trecognition: null,\n\t\t\t/** Text already in the box when dictation started, so it is not lost. */\n\t\t\ttextBeforeDictation: '',\n\t\t\t/** Timer that releases the mic after a silence. null when not armed. */\n\t\t\tsilenceTimer: null,\n\t\t\t/** Whether a hands-free conversation is running. */\n\t\t\tconversing: false,\n\t\t\t/** The local-engine session, or null when idle. */\n\t\t\tlocalSession: null,\n\t\t\t/** Whether the local engine is recording right now. */\n\t\t\tlocalRecording: false,\n\t\t\t/**\n\t\t\t * Whether a recorded clip is being transcribed.\n\t\t\t *\n\t\t\t * Its own state, not folded into `localRecording`: the microphone is\n\t\t\t * already closed by then, and showing \"recording\" through a\n\t\t\t * multi-second upload is the same lie the mic icon used to tell.\n\t\t\t */\n\t\t\ttranscribing: false,\n\t\t\t/** Last dictation failure, shown to the user. '' when none. */\n\t\t\tdictationError: '',\n\t\t\t/** Timer clearing that message, so it does not outlive its moment. */\n\t\t\tdictationErrorTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether this browser can do speech recognition at all.\n\t\t *\n\t\t * Checked rather than assumed: Chrome and Safari expose it (Safari only\n\t\t * under the `webkit` prefix) and Firefox does not expose it at all.\n\t\t *\n\t\t * ⚠️ This answers \"is the BROWSER engine available\", which since the\n\t\t * per-agent policy landed is no longer the same question as \"may this\n\t\t * agent dictate\" — see `dictationDecision`. Used only by the browser\n\t\t * path's own guard.\n\t\t *\n\t\t * @return {boolean} true when a SpeechRecognition constructor exists.\n\t\t */\n\t\tspeechSupported() {\n\t\t\treturn browserRecognitionUsable()\n\t\t},\n\n\t\t/**\n\t\t * Why dictation cannot run here, or '' when it can.\n\t\t *\n\t\t * 🔴 The constructor EXISTING is not the capability. On an insecure\n\t\t * origin Chrome still exposes `webkitSpeechRecognition`, so a\n\t\t * constructor check passes, and then `start()` fires `onerror:\n\t\t * not-allowed` immediately — measured on this instance over http://,\n\t\t * where `isSecureContext` is false and `navigator.mediaDevices` is\n\t\t * undefined. The first version of this button did exactly that and\n\t\t * appeared to do nothing at all when pressed.\n\t\t *\n\t\t * Reported rather than hidden: the button vanishing reads as a missing\n\t\t * feature, while a disabled button that says why reads as a\n\t\t * deployment fact the user can act on (serve over https).\n\t\t *\n\t\t * @return {string} A reason to show, or '' when dictation is available.\n\t\t */\n\t\tspeechBlockedReason() {\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\treturn this.cnTranslate(this.dictationDecision.reason)\n\t\t\t}\n\t\t\tif (typeof window !== 'undefined' && window.isSecureContext === false) {\n\t\t\t\treturn this.cnTranslate('Dictation needs a secure (https) connection')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Which engine this agent's dictation may use, and why not when it may not.\n\t\t *\n\t\t * The DECISION, not a preference — `startDictation` runs exactly what\n\t\t * this names. See composables/aiSpeechPolicy.js for the rule that matters:\n\t\t * an agent pinned to the private engine never falls back to the browser's,\n\t\t * because that fallback would send confidential audio to a cloud service.\n\t\t *\n\t\t * @return {{engine: string, reason: string}} The decision.\n\t\t */\n\t\tdictationDecision() {\n\t\t\treturn resolveDictationEngine(\n\t\t\t\t{ inputEngine: this.speechInputEngine },\n\t\t\t\t{\n\t\t\t\t\tbrowserUsable: browserRecognitionUsable(),\n\t\t\t\t\tlocalUsable: this.localSpeechAvailable === true && browserRecordingUsable(),\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the mic button is offered at all.\n\t\t *\n\t\t * ⚠️ HIDDEN ONLY WHEN THE AGENT SAYS SO. An unavailable engine still\n\t\t * renders the button, disabled and carrying its reason: a control that\n\t\t * vanishes reads as a missing feature, while one that says \"this agent\n\t\t * may only use the private speech service, and it is unavailable\" reads\n\t\t * as a fact somebody can act on. The exception is `off`, where the agent\n\t\t * has decided there is no dictation — then there is nothing to explain.\n\t\t *\n\t\t * @return {boolean} true when the button should render.\n\t\t */\n\t\tspeechButtonVisible() {\n\t\t\tif (this.speechInputEngine === SPEECH_OFF) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.dictationDecision.engine !== SPEECH_OFF\n\t\t\t\t|| this.dictationDecision.reason !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether dictation is currently running on either engine.\n\t\t *\n\t\t * @return {boolean} true while the microphone is open.\n\t\t */\n\t\tmicIsOpen() {\n\t\t\treturn this.listening === true || this.localRecording === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the conversation control is offered.\n\t\t *\n\t\t * Both conditions matter: the agent has to allow it, AND there has to be\n\t\t * an engine that can hear you. A hands-free control on an agent with no\n\t\t * working microphone is a button that can only disappoint.\n\t\t *\n\t\t * @return {boolean} true when the control should render.\n\t\t */\n\t\tconversationButtonVisible() {\n\t\t\treturn this.conversationEnabled === true\n\t\t\t\t&& this.dictationDecision.engine !== SPEECH_OFF\n\t\t},\n\n\t\t/**\n\t\t * The conversation control's label.\n\t\t *\n\t\t * Says what pressing it will DO, including the part people need warning\n\t\t * about — that a pause sends the message.\n\t\t *\n\t\t * @return {string} The label.\n\t\t */\n\t\tconverseButtonLabel() {\n\t\t\treturn this.conversing === true\n\t\t\t\t? this.cnTranslate('End the spoken conversation')\n\t\t\t\t: this.cnTranslate('Talk to the agent — your turn is sent when you stop speaking')\n\t\t},\n\n\t\t/**\n\t\t * The mic button's label and tooltip.\n\t\t *\n\t\t * Names the ENGINE when dictation is idle and the private one would be\n\t\t * used, because \"which service is about to hear me\" is not something a\n\t\t * user can discover any other way — and the difference between the two\n\t\t * is whether their words leave the building.\n\t\t *\n\t\t * @return {string} The label.\n\t\t */\n\t\tmicButtonLabel() {\n\t\t\tif (this.speechBlockedReason !== '') {\n\t\t\t\treturn this.speechBlockedReason\n\t\t\t}\n\t\t\tif (this.transcribing === true) {\n\t\t\t\treturn this.cnTranslate('Transcribing…')\n\t\t\t}\n\t\t\tif (this.micIsOpen === true) {\n\t\t\t\treturn this.cnTranslate('Stop dictation')\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\treturn this.cnTranslate('Dictate message (private, on this instance)')\n\t\t\t}\n\t\t\treturn this.cnTranslate('Dictate message')\n\t\t},\n\n\t\tisTextEmpty() {\n\t\t\treturn !this.inputText || !this.inputText.trim()\n\t\t},\n\n\t\t/** Send is only blocked when there's neither text nor an attachment. */\n\t\tisSendDisabled() {\n\t\t\treturn this.isTextEmpty && this.attachments.length === 0\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\t// A recogniser outliving its component keeps the microphone open — the\n\t\t// indicator stays lit in the browser chrome with nothing on screen\n\t\t// explaining why.\n\t\tthis.conversing = false\n\t\tthis.stopDictation()\n\t\tif (this.dictationErrorTimer !== null) {\n\t\t\tclearTimeout(this.dictationErrorTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Function ref for the hidden file input; a method so the binding is\n\t\t * stable across renders.\n\t\t *\n\t\t * @param {HTMLInputElement|null} el The element, or null on unmount.\n\t\t * @return {void}\n\t\t */\n\t\tfileInputRef(el) {\n\t\t\tthis.fileInputEl = el || null\n\t\t},\n\n\t\t/**\n\t\t * Show a dictation failure, and take it away again.\n\t\t *\n\t\t * A banner with no lifetime is a banner that becomes furniture: the\n\t\t * first version stayed on screen for the rest of the session, sitting\n\t\t * above the composer long after the moment it described, so it read as a\n\t\t * permanent state of the chat rather than as the result of a click.\n\t\t *\n\t\t * @param {string} message What went wrong.\n\t\t * @return {void}\n\t\t */\n\t\tshowDictationError(message) {\n\t\t\tthis.dictationError = message\n\t\t\tif (this.dictationErrorTimer !== null) {\n\t\t\t\tclearTimeout(this.dictationErrorTimer)\n\t\t\t}\n\t\t\tthis.dictationErrorTimer = setTimeout(() => {\n\t\t\t\tthis.dictationError = ''\n\t\t\t\tthis.dictationErrorTimer = null\n\t\t\t}, DICTATION_ERROR_TIMEOUT)\n\t\t},\n\n\t\t/**\n\t\t * (Re)start the silence countdown that releases the microphone.\n\t\t *\n\t\t * Called on every result — interim ones included, so a long sentence\n\t\t * keeps the mic open while the words are still arriving rather than\n\t\t * being cut off between clauses.\n\t\t *\n\t\t * ⚠️ ARMED BY SPEECH, NOT BY `start()`. Arming it when dictation begins\n\t\t * closes the microphone on somebody who pressed the button and then\n\t\t * spent four seconds deciding what to say — the exact moment the\n\t\t * feature exists to serve. Until the first result arrives the engine's\n\t\t * own `no-speech` error is the backstop, and it says something useful.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tarmSilenceTimer() {\n\t\t\tthis.clearSilenceTimer()\n\t\t\tif (this.dictationSilenceTimeout <= 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.silenceTimer = setTimeout(() => {\n\t\t\t\tthis.silenceTimer = null\n\t\t\t\t// Only the microphone is released. The text stays put.\n\t\t\t\tthis.stopDictation()\n\t\t\t\t// …unless this silence is the end of a conversational turn, which\n\t\t\t\t// is the ONE case where a pause sends — and the user pressed a\n\t\t\t\t// separate control to say so.\n\t\t\t\tthis.completeConversationTurn()\n\t\t\t}, this.dictationSilenceTimeout)\n\t\t},\n\n\t\t/**\n\t\t * Cancel a pending silence countdown.\n\t\t *\n\t\t * ⚠️ A LEFTOVER TIMER OUTLIVES THE DICTATION THAT ARMED IT and would\n\t\t * close the NEXT one mid-sentence, seconds after it started, with no\n\t\t * silence involved. So this is called from every path that ends a\n\t\t * dictation — stop, `onend`, `onerror`, unmount — not only the tidy one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearSilenceTimer() {\n\t\t\tif (this.silenceTimer !== null) {\n\t\t\t\tclearTimeout(this.silenceTimer)\n\t\t\t\tthis.silenceTimer = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start dictation, or stop it if already running.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleDictation() {\n\t\t\t// The reason is shown rather than the press being ignored: a button\n\t\t\t// that does nothing teaches nothing, and this one has something\n\t\t\t// specific to say — that the agent forbids the fast engine and the\n\t\t\t// private one is down.\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.showDictationError(this.cnTranslate(this.dictationDecision.reason))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.micIsOpen) {\n\t\t\t\tthis.stopDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.startLocalDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.startDictation()\n\t\t},\n\n\t\t/**\n\t\t * Start or end a hands-free conversation.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttoggleConversation() {\n\t\t\tif (this.conversing === true) {\n\t\t\t\tthis.endConversation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.showDictationError(this.cnTranslate(this.dictationDecision.reason))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.conversing = true\n\t\t\tthis.$emit('conversation-state', true)\n\t\t\tthis.beginConversationTurn()\n\t\t},\n\n\t\t/**\n\t\t * Stop conversing and release the microphone.\n\t\t *\n\t\t * ⚠️ The FIRST thing it does is clear the flag, before stopping\n\t\t * dictation. `stopDictation` runs the same paths a silence does, and\n\t\t * with the flag still set the engine's own stop event would be read as\n\t\t * \"your turn ended\" and send whatever was captured — so pressing \"end\n\t\t * conversation\" would post one last message on the way out.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tendConversation() {\n\t\t\tthis.conversing = false\n\t\t\tthis.$emit('conversation-state', false)\n\t\t\tthis.stopDictation()\n\t\t},\n\n\t\t/**\n\t\t * Open the microphone for the user's next turn.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tbeginConversationTurn() {\n\t\t\tif (this.conversing === false || this.disabled === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.dictationDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.startLocalDictation()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.startDictation()\n\t\t},\n\n\t\t/**\n\t\t * Called by the panel once the agent has finished answering — and, when\n\t\t * the reply is spoken aloud, once it has finished being spoken.\n\t\t *\n\t\t * 🔴 THE MICROPHONE MUST NOT REOPEN WHILE THE AGENT IS TALKING. It would\n\t\t * record the reply through the speakers and hand it back as the user's\n\t\t * next turn, and with auto-send on a silence that becomes a conversation\n\t\t * the agent is having with itself. Which is why the resume is driven\n\t\t * from outside rather than from a timer in here: only the side that owns\n\t\t * the speaking knows when it stopped.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresumeConversation() {\n\t\t\tthis.beginConversationTurn()\n\t\t},\n\n\t\t/**\n\t\t * End the user's turn: send what was heard.\n\t\t *\n\t\t * Nothing is sent when nothing was heard — an empty turn means the\n\t\t * microphone opened, caught silence and closed, which should leave the\n\t\t * conversation waiting rather than post a blank message.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcompleteConversationTurn() {\n\t\t\tif (this.conversing === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isSendDisabled === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.handleSend()\n\t\t},\n\n\t\t/**\n\t\t * Dictate through the instance's own speech service.\n\t\t *\n\t\t * No partial text on the way — whisper answers a finished clip — so the\n\t\t * feedback is the recording state and then an explicit \"transcribing\"\n\t\t * one. The result is APPENDED to whatever is already in the box, the\n\t\t * same as the browser path, so dictating twice builds a message rather\n\t\t * than replacing it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartLocalDictation() {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.dictationError = ''\n\t\t\tthis.localSession = createLocalDictation({\n\t\t\t\tsilenceTimeout: this.dictationSilenceTimeout,\n\t\t\t\ttranscribe: (blob) => this.uploadForTranscription(blob),\n\t\t\t\tonTranscript: (text) => {\n\t\t\t\t\tconst existing = this.inputText.trim()\n\t\t\t\t\tthis.inputText = (existing === '') ? text : `${existing} ${text}`\n\t\t\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t\t\t\tthis.$emit('dictation-complete', text)\n\t\t\t\t\t// The local engine has no silence event of its own to hang\n\t\t\t\t\t// this on — the transcript ARRIVING is the end of the turn,\n\t\t\t\t\t// seconds after the speaker actually stopped.\n\t\t\t\t\tthis.completeConversationTurn()\n\t\t\t\t},\n\t\t\t\tonError: (message) => {\n\t\t\t\t\tthis.showDictationError(this.cnTranslate(message))\n\t\t\t\t},\n\t\t\t\tonStateChange: (state) => {\n\t\t\t\t\tthis.localRecording = (state === 'recording')\n\t\t\t\t\tthis.transcribing = (state === 'transcribing')\n\t\t\t\t},\n\t\t\t})\n\t\t\tthis.localSession.start()\n\t\t},\n\n\t\t/**\n\t\t * Send a recorded clip to the backend for transcription.\n\t\t *\n\t\t * @param {Blob} blob The recorded audio.\n\t\t * @return {Promise<{text: string}>} The transcript.\n\t\t */\n\t\tasync uploadForTranscription(blob) {\n\t\t\tconst formData = new FormData()\n\t\t\tformData.append('audio', blob, 'dictation.webm')\n\t\t\t// The page's language, for the same reason the browser engine is\n\t\t\t// given it: auto-detection misfires on short utterances, and a\n\t\t\t// Dutch sentence detected as English comes back as nonsense rather\n\t\t\t// than as an error.\n\t\t\tif (typeof document !== 'undefined' && document.documentElement.lang) {\n\t\t\t\tformData.append('language', document.documentElement.lang)\n\t\t\t}\n\t\t\tconst response = await axios.post(speechTranscriptionsUrl(this.chatAppId), formData)\n\n\t\t\treturn (response && response.data) || { text: '' }\n\t\t},\n\n\t\t/**\n\t\t * Begin capturing speech into the message box.\n\t\t *\n\t\t * Interim results are shown as they arrive so the user can see it is\n\t\t * working — dictation with no visible feedback reads as broken. Whatever\n\t\t * was already typed is preserved and appended to, rather than replaced.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartDictation() {\n\t\t\tif (!this.speechSupported || this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.dictationError = ''\n\t\t\tconst Recognition = window.SpeechRecognition || window.webkitSpeechRecognition\n\t\t\tconst recognition = new Recognition()\n\t\t\trecognition.continuous = true\n\t\t\trecognition.interimResults = true\n\t\t\t// Follow the page's language rather than hardcoding one: this ships to\n\t\t\t// Dutch and English instances alike.\n\t\t\trecognition.lang = (typeof document !== 'undefined' && document.documentElement.lang)\n\t\t\t\t? document.documentElement.lang\n\t\t\t\t: 'en-US'\n\n\t\t\tthis.textBeforeDictation = this.inputText\n\n\t\t\trecognition.onresult = (event) => {\n\t\t\t\tlet finalText = ''\n\t\t\t\tlet interimText = ''\n\t\t\t\tfor (let i = event.resultIndex; i < event.results.length; i++) {\n\t\t\t\t\tconst result = event.results[i]\n\t\t\t\t\tif (result.isFinal) {\n\t\t\t\t\t\tfinalText += result[0].transcript\n\t\t\t\t\t} else {\n\t\t\t\t\t\tinterimText += result[0].transcript\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (finalText !== '') {\n\t\t\t\t\tthis.textBeforeDictation = `${this.textBeforeDictation}${finalText}`.replace(/\\s+/g, ' ')\n\t\t\t\t}\n\t\t\t\tthis.inputText = `${this.textBeforeDictation}${interimText}`.trimStart()\n\t\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t\t\tthis.armSilenceTimer()\n\t\t\t}\n\n\t\t\t// `onend` fires for a user stop AND for the engine giving up on\n\t\t\t// silence, so the button state is driven from here rather than from\n\t\t\t// the click — otherwise the button lies after an idle timeout.\n\t\t\trecognition.onend = () => {\n\t\t\t\tthis.clearSilenceTimer()\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\n\t\t\t// SHOW the failure. Silently resetting `listening` is what made the\n\t\t\t// button look inert: the user pressed it, nothing changed, and there\n\t\t\t// was nowhere to find out why. `not-allowed` is the common one — an\n\t\t\t// insecure origin or a denied microphone permission.\n\t\t\trecognition.onerror = (event) => {\n\t\t\t\tconst code = (event && event.error) ? String(event.error) : 'unknown'\n\t\t\t\tthis.showDictationError((code === 'not-allowed')\n\t\t\t\t\t? this.cnTranslate('Microphone blocked — allow access, or use https')\n\t\t\t\t\t: this.cnTranslate('Dictation stopped: ') + code)\n\t\t\t\tthis.clearSilenceTimer()\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\trecognition.start()\n\t\t\t\tthis.recognition = recognition\n\t\t\t\tthis.listening = true\n\t\t\t} catch (e) {\n\t\t\t\t// `start()` throws if called twice, and on some browsers when the\n\t\t\t\t// origin is not permitted. Either way say so rather than resetting\n\t\t\t\t// in silence.\n\t\t\t\tthis.showDictationError(this.cnTranslate('Dictation could not start: ') + (e.message || 'unknown'))\n\t\t\t\tthis.listening = false\n\t\t\t\tthis.recognition = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Stop dictation and release the microphone.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopDictation() {\n\t\t\tthis.clearSilenceTimer()\n\t\t\tif (this.localSession !== null) {\n\t\t\t\t// The local session owns its own teardown, including releasing\n\t\t\t\t// the microphone BEFORE the transcript request goes out.\n\t\t\t\tthis.localSession.stop()\n\t\t\t\tthis.localSession = null\n\t\t\t\tthis.localRecording = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.recognition) {\n\t\t\t\tthis.listening = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.recognition.stop()\n\t\t\t} catch {\n\t\t\t\t// Already stopped; nothing to release.\n\t\t\t}\n\t\t\tthis.recognition = null\n\t\t\tthis.listening = false\n\t\t},\n\n\t\thandleEnter() {\n\t\t\tif (this.disabled || this.isSendDisabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.handleSend()\n\t\t},\n\n\t\thandleShiftEnter() {\n\t\t\t// Default browser behaviour inserts a newline — we don't need to prevent it.\n\t\t\t// But we do need to trigger autoGrow after the character is inserted.\n\t\t\tthis.$nextTick(() => this.autoGrow())\n\t\t},\n\n\t\thandleSend() {\n\t\t\tif (this.disabled || this.isSendDisabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst text = this.inputText.trim()\n\t\t\tconst attachments = this.attachments.slice()\n\t\t\tthis.inputText = ''\n\t\t\tthis.attachments = []\n\t\t\tthis.uploadError = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.autoGrow()\n\t\t\t})\n\t\t\tthis.$emit('send', { text, attachments })\n\t\t},\n\n\t\tautoGrow() {\n\t\t\tconst el = this.$refs.textarea\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Reset height first so shrinking works\n\t\t\tel.style.height = 'auto'\n\t\t\t// Clamp to 6-line max (~1.5em per line + padding)\n\t\t\tconst maxHeight = parseInt(getComputedStyle(el).lineHeight || '24', 10) * 6 + 24\n\t\t\tel.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'\n\t\t},\n\n\t\t/** Focus the textarea — called by the parent panel on open. */\n\t\tfocus() {\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.textarea) {\n\t\t\t\t\tthis.$refs.textarea.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** Open the hidden file input in response to the paperclip button. */\n\t\topenFilePicker() {\n\t\t\tif (this.disabled || this.uploading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploadError = ''\n\t\t\tif (this.fileInputEl) {\n\t\t\t\tthis.fileInputEl.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Upload the picked file to the chat backend's attachments endpoint and,\n\t\t * on success, add it as a removable chip. Surfaces the backend's\n\t\t * `{ error }` message inline on a 400 rejection (oversized, or not\n\t\t * text-decodable) instead of silently dropping the file.\n\t\t *\n\t\t * @param {Event} event The file input's change event.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onFileSelected(event) {\n\t\t\tconst file = event.target && event.target.files && event.target.files[0]\n\t\t\tevent.target.value = ''\n\t\t\tif (!file) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploading = true\n\t\t\tthis.uploadError = ''\n\t\t\ttry {\n\t\t\t\tconst formData = new FormData()\n\t\t\t\tformData.append('file', file)\n\t\t\t\tconst response = await axios.post(attachmentsUrl(this.chatAppId), formData)\n\t\t\t\tconst { path, name } = response.data || {}\n\t\t\t\tthis.attachments.push({ path, name: name || file.name })\n\t\t\t} catch (err) {\n\t\t\t\tconst backendMessage = err && err.response && err.response.status === 400\n\t\t\t\t\t? err.response.data && err.response.data.error\n\t\t\t\t\t: null\n\t\t\t\tthis.uploadError = backendMessage || this.cnTranslate('Could not attach file')\n\t\t\t} finally {\n\t\t\t\tthis.uploading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pick files already in Nextcloud Files and add each as a chip carrying\n\t\t * its file id; they are sent by id and never uploaded. A dismissed\n\t\t * picker (which rejects in some dialog versions) is a no-op.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync chooseFromFiles() {\n\t\t\tif (this.disabled || this.uploading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.uploadError = ''\n\t\t\ttry {\n\t\t\t\tconst picker = getFilePickerBuilder(this.cnTranslate('Choose from Files'))\n\t\t\t\t\t.setMultiSelect(true)\n\t\t\t\t\t.setType(FilePickerType.Choose)\n\t\t\t\t\t.allowDirectories(false)\n\t\t\t\t\t.setModal(true)\n\t\t\t\t\t.build()\n\t\t\t\tconst nodes = await picker.pickNodes()\n\t\t\t\tfor (const node of (Array.isArray(nodes) ? nodes : [])) {\n\t\t\t\t\tconst fileId = node.fileid ?? node.attributes?.fileid\n\t\t\t\t\tconst path = node.path || ''\n\t\t\t\t\tif (fileId === null || fileId === undefined || this.attachments.some((a) => a.fileId === fileId)) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tthis.attachments.push({ fileId, path, name: node.basename || path.split('/').pop() })\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.debug('[CnAiInput] file picker closed', e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove a pending attachment chip before it's sent.\n\t\t *\n\t\t * @param {number} index Index into `attachments`.\n\t\t */\n\t\tremoveAttachment(index) {\n\t\t\tthis.attachments.splice(index, 1)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-input {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-ai-input__error {\n\tmargin: 0;\n}\n\n.cn-ai-input__chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 6px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-ai-input__chip {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tmax-width: 220px;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius-pill, 16px);\n\tbackground: var(--color-background-hover);\n\tfont-size: 13px;\n}\n\n.cn-ai-input__chip-icon {\n\tflex-shrink: 0;\n\topacity: 0.7;\n}\n\n.cn-ai-input__chip-name {\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n\n.cn-ai-input__chip-remove {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 0;\n\tborder: none;\n\tbackground: transparent;\n\tcolor: inherit;\n\tcursor: pointer;\n\topacity: 0.7;\n}\n\n/* `:focus-visible` and `:disabled` are declared BEFORE the compound\n `:hover:not(:disabled)` rule: stylelint's no-descending-specificity wants\n lower-specificity selectors first, and `:hover:not(:disabled)` outranks\n both. Ordering only — the cascade result is unchanged. */\n.cn-ai-input__chip-remove:focus-visible {\n\topacity: 1;\n}\n\n.cn-ai-input__chip-remove:disabled {\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__chip-remove:hover:not(:disabled) {\n\topacity: 1;\n}\n\n.cn-ai-input__row {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: flex-end;\n}\n\n/* `stacked`: in a card the box keeps the line, dictation and attach wrap\n under it (DqZaakAssistent \"Vraag iets over deze zaak\" + \"Vragen\"). */\n.cn-ai-input--stacked {\n\tpadding: 12px 18px;\n}\n\n.cn-ai-input--stacked .cn-ai-input__row {\n\tflex-wrap: wrap;\n\talign-items: center;\n}\n\n.cn-ai-input--stacked .cn-ai-input__textarea {\n\torder: -2;\n\tflex: 1 1 calc(100% - 104px);\n\tmin-width: 0;\n\tmin-height: 40px;\n}\n\n.cn-ai-input--stacked .cn-ai-input__send-button {\n\torder: -1;\n}\n\n.cn-ai-input__file-input {\n\tdisplay: none;\n}\n\n/* Mic matches the paperclip exactly — they are peers in the row, and any\n difference in size or weight would read as one being the primary action. */\n.cn-ai-input__mic-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n/* Declared BEFORE the hover rule below, which is more specific\n (`:hover:not(:disabled)`). Source order has to follow specificity here or the\n cascade's outcome depends on which rule happens to be written last — the\n thing `no-descending-specificity` exists to stop. */\n.cn-ai-input__mic-button:disabled {\n\topacity: .5;\n\tcursor: default;\n}\n\n.cn-ai-input__mic-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* Recording is a STATE, not a hover: it stays visible while the user speaks and\n looks away from the button. Error colour because it is the \"something is\n live\" signal, and the icon fills in beside it — it does NOT swap to a\n struck-through mic, which is what this comment used to describe and what made\n a listening composer look muted. */\n.cn-ai-input__mic-button--recording {\n\tborder-color: var(--color-error);\n\tbackground: var(--color-error);\n\tcolor: var(--color-primary-text, #fff);\n}\n\n/* 🔴 THE BLOCKED LOOK THE COMMENT ABOVE THE BUTTON ALREADY PROMISED.\n `cn-ai-input__mic-button--blocked` was bound in the template and styled\n NOWHERE, so a microphone that cannot run — an agent pinned to the private\n engine while the speech service is down, or a browser with no recognition —\n looked exactly like a working one until you hovered it for the tooltip. A\n class set and never read is a comment, not a state.\n\n ⚠️ Muted rather than `disabled`, and NOT `pointer-events: none`. The button\n deliberately keeps a live click handler: a natively disabled control\n suppresses hover, so its `title` can never appear and it refuses without ever\n saying why. It must stay clickable in order to explain itself. */\n.cn-ai-input__mic-button--blocked {\n\topacity: .5;\n\tcursor: not-allowed;\n}\n\n/* The conversation control sits beside the mic and matches it in weight —\n neither is the primary action, and a bigger one would suggest the hands-free\n mode is the normal way to use the composer rather than a deliberate choice. */\n.cn-ai-input__converse-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__converse-button:disabled {\n\topacity: .5;\n\tcursor: default;\n}\n\n.cn-ai-input__converse-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* Primary rather than error colour: a live conversation is a mode the user is\n in, not a recording warning — the microphone beside it carries that. */\n.cn-ai-input__converse-button--active {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n}\n\n.cn-ai-input__attach-menu {\n\tflex: 0 0 auto;\n}\n\n.cn-ai-input__attach-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__attach-button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__attach-button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__attach-button:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__textarea {\n\tflex: 1;\n\tmin-height: 36px;\n\tmax-height: calc(6 * 1.5em + 24px);\n\tpadding: 8px;\n\tresize: none;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tline-height: 1.5;\n\toverflow-y: auto;\n}\n\n.cn-ai-input__textarea:focus {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 0;\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-ai-input__textarea:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n\n.cn-ai-input__send-button {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 36px;\n\theight: 36px;\n\tpadding: 0;\n\tborder: none;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n\tcursor: pointer;\n}\n\n.cn-ai-input__send-button--labelled {\n\twidth: auto;\n\tmin-height: 40px;\n\tpadding: 0 14px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n}\n\n.cn-ai-input__send-button:hover:not(:disabled) {\n\tbackground: var(--color-primary-element-hover, var(--color-primary-element));\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__send-button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-ai-input__send-button:disabled {\n\topacity: 0.6;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["axios"],"mappings":"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"}
@@ -1,21 +1,17 @@
1
- import { resolveComponent, openBlock, createElementBlock, createBlock, withCtx, createTextVNode, toDisplayString, createCommentVNode, Fragment, renderList, createVNode, createElementVNode, normalizeClass, withDirectives, withKeys, withModifiers, vModelText } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, createBlock, withCtx, createTextVNode, toDisplayString, createCommentVNode, Fragment, renderList, createVNode, createElementVNode, withDirectives, withKeys, withModifiers, vModelText } from 'vue';
2
2
 
3
3
  const _hoisted_1 = {
4
- class: "cn-ai-input",
5
- "data-testid": "cn-ai-input"
6
- };
7
- const _hoisted_2 = {
8
4
  key: 2,
9
5
  class: "cn-ai-input__chips",
10
6
  "data-testid": "cn-ai-input-chips"
11
7
  };
12
- const _hoisted_3 = { class: "cn-ai-input__chip-name" };
13
- const _hoisted_4 = ["aria-label", "disabled", "data-testid", "onClick"];
14
- const _hoisted_5 = { class: "cn-ai-input__row" };
15
- const _hoisted_6 = ["aria-label", "title", "aria-pressed", "aria-disabled", "disabled", "data-engine"];
16
- const _hoisted_7 = ["aria-label", "title", "aria-pressed", "disabled"];
17
- const _hoisted_8 = ["aria-label", "disabled", "placeholder"];
18
- const _hoisted_9 = ["aria-label", "disabled"];
8
+ const _hoisted_2 = { class: "cn-ai-input__chip-name" };
9
+ const _hoisted_3 = ["aria-label", "disabled", "data-testid", "onClick"];
10
+ const _hoisted_4 = { class: "cn-ai-input__row" };
11
+ const _hoisted_5 = ["aria-label", "title", "aria-pressed", "aria-disabled", "disabled", "data-engine"];
12
+ const _hoisted_6 = ["aria-label", "title", "aria-pressed", "disabled"];
13
+ const _hoisted_7 = ["aria-label", "disabled", "placeholder"];
14
+ const _hoisted_8 = ["aria-label", "disabled"];
19
15
 
20
16
  function render(_ctx, _cache, $props, $setup, $data, $options) {
21
17
  const _component_NcNoteCard = resolveComponent("NcNoteCard");
@@ -32,7 +28,10 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
32
28
  const _component_NcActions = resolveComponent("NcActions");
33
29
  const _component_Send = resolveComponent("Send");
34
30
 
35
- return (openBlock(), createElementBlock("div", _hoisted_1, [
31
+ return (openBlock(), createElementBlock("div", {
32
+ class: normalizeClass(["cn-ai-input", { 'cn-ai-input--stacked': $props.stacked }]),
33
+ "data-testid": "cn-ai-input"
34
+ }, [
36
35
  ($data.uploadError)
37
36
  ? (openBlock(), createBlock(_component_NcNoteCard, {
38
37
  key: 0,
@@ -60,7 +59,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
60
59
  }))
61
60
  : createCommentVNode("v-if", true),
62
61
  ($data.attachments.length)
63
- ? (openBlock(), createElementBlock("ul", _hoisted_2, [
62
+ ? (openBlock(), createElementBlock("ul", _hoisted_1, [
64
63
  (openBlock(true), createElementBlock(Fragment, null, renderList($data.attachments, (attachment, index) => {
65
64
  return (openBlock(), createElementBlock("li", {
66
65
  key: attachment.fileId !== undefined ? 'f' + attachment.fileId : attachment.path,
@@ -70,7 +69,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
70
69
  size: 14,
71
70
  class: "cn-ai-input__chip-icon"
72
71
  }),
73
- createElementVNode("span", _hoisted_3, toDisplayString(attachment.name), 1 /* TEXT */),
72
+ createElementVNode("span", _hoisted_2, toDisplayString(attachment.name), 1 /* TEXT */),
74
73
  createElementVNode("button", {
75
74
  type: "button",
76
75
  class: "cn-ai-input__chip-remove",
@@ -80,12 +79,12 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
80
79
  onClick: $event => ($options.removeAttachment(index))
81
80
  }, [
82
81
  createVNode(_component_Close, { size: 14 })
83
- ], 8 /* PROPS */, _hoisted_4)
82
+ ], 8 /* PROPS */, _hoisted_3)
84
83
  ]))
85
84
  }), 128 /* KEYED_FRAGMENT */))
86
85
  ]))
87
86
  : createCommentVNode("v-if", true),
88
- createElementVNode("div", _hoisted_5, [
87
+ createElementVNode("div", _hoisted_4, [
89
88
  ($options.speechButtonVisible)
90
89
  ? (openBlock(), createElementBlock("button", {
91
90
  key: 0,
@@ -118,7 +117,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
118
117
  key: 2,
119
118
  size: 20
120
119
  }))
121
- ], 10 /* CLASS, PROPS */, _hoisted_6))
120
+ ], 10 /* CLASS, PROPS */, _hoisted_5))
122
121
  : createCommentVNode("v-if", true),
123
122
  ($options.conversationButtonVisible)
124
123
  ? (openBlock(), createElementBlock("button", {
@@ -141,7 +140,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
141
140
  key: 1,
142
141
  size: 20
143
142
  }))
144
- ], 10 /* CLASS, PROPS */, _hoisted_7))
143
+ ], 10 /* CLASS, PROPS */, _hoisted_6))
145
144
  : createCommentVNode("v-if", true),
146
145
  createVNode(_component_NcActions, {
147
146
  class: "cn-ai-input__attach-menu",
@@ -212,7 +211,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
212
211
  _cache[5] || (_cache[5] = withKeys(withModifiers((...args) => ($options.handleShiftEnter && $options.handleShiftEnter(...args)), ["shift","exact"]), ["enter"]))
213
212
  ],
214
213
  onInput: _cache[6] || (_cache[6] = (...args) => ($options.autoGrow && $options.autoGrow(...args)))
215
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_8), [
214
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_7), [
216
215
  [vModelText, $data.inputText]
217
216
  ]),
218
217
  createElementVNode("button", {
@@ -236,9 +235,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
236
235
  key: 2,
237
236
  size: 20
238
237
  }))
239
- ], 10 /* CLASS, PROPS */, _hoisted_9)
238
+ ], 10 /* CLASS, PROPS */, _hoisted_8)
240
239
  ])
241
- ]))
240
+ ], 2 /* CLASS */))
242
241
  }
243
242
 
244
243
  export { render };