@conduction/nextcloud-vue 2.119.0 → 2.121.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 (179) hide show
  1. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
  2. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
  3. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
  4. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
  6. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
  8. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
  9. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
  10. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
  12. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
  13. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  14. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
  16. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
  20. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  21. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
  22. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
  23. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
  24. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
  25. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
  26. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
  27. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  28. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
  29. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  31. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
  32. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
  33. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
  34. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
  35. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
  36. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
  37. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
  38. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
  39. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
  40. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
  41. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
  42. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
  43. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
  44. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
  46. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
  48. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
  50. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
  51. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
  52. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
  53. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
  54. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
  55. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
  56. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
  57. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
  58. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
  59. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
  60. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
  61. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
  62. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
  63. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
  64. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
  65. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
  66. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
  67. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
  68. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
  69. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
  70. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
  71. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
  72. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
  73. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
  74. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
  75. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
  76. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
  77. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
  78. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
  79. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
  80. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
  81. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
  82. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
  83. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
  84. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
  85. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
  86. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
  87. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
  88. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
  89. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
  90. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
  91. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
  92. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
  99. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
  101. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  102. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
  103. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  104. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
  105. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  106. package/dist/esm/composables/indexSources.js +116 -16
  107. package/dist/esm/composables/indexSources.js.map +1 -1
  108. package/dist/esm/index.js +9 -0
  109. package/dist/esm/index.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +207 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +207 -1
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +951 -16
  116. package/dist/esm/utils/aiFeatures.js +245 -0
  117. package/dist/esm/utils/aiFeatures.js.map +1 -0
  118. package/dist/esm/utils/aiSuggestionTracker.js +182 -0
  119. package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
  120. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  121. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  122. package/dist/esm/utils/objectPermissions.js +552 -0
  123. package/dist/esm/utils/objectPermissions.js.map +1 -0
  124. package/dist/esm/utils/objectTranslations.js +180 -0
  125. package/dist/esm/utils/objectTranslations.js.map +1 -0
  126. package/dist/esm/utils/stateMachineValidation.js +254 -0
  127. package/dist/esm/utils/stateMachineValidation.js.map +1 -0
  128. package/dist/esm/utils/widgetDispatch.js +3 -1
  129. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  130. package/dist/nextcloud-vue.cjs.js +23038 -14567
  131. package/dist/nextcloud-vue.cjs.js.map +2 -2
  132. package/dist/nextcloud-vue.css +951 -16
  133. package/l10n/en.json +207 -1
  134. package/l10n/nl.json +207 -1
  135. package/package.json +1 -1
  136. package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
  137. package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
  138. package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
  139. package/src/components/CnAiFeaturesWidget/index.js +14 -0
  140. package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
  141. package/src/components/CnIndexPage/useNamedSource.js +9 -0
  142. package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
  143. package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
  144. package/src/components/CnObjectPermissionsTab/index.js +13 -0
  145. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
  146. package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
  147. package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
  148. package/src/components/CnObjectTranslationsWidget/index.js +13 -0
  149. package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
  150. package/src/components/CnReadinessChecklist/index.js +9 -0
  151. package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
  152. package/src/components/CnSchemaRulesWidget/index.js +10 -0
  153. package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
  154. package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
  155. package/src/components/CnSearchIndexStatus/index.js +10 -0
  156. package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
  157. package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
  158. package/src/components/CnShippedConfigurationSection/index.js +9 -0
  159. package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
  160. package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
  161. package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
  162. package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
  163. package/src/components/CnStateMachineEditor/index.js +8 -0
  164. package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
  165. package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
  166. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
  167. package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
  169. package/src/components/index.js +8 -0
  170. package/src/composables/indexSources.js +116 -16
  171. package/src/css/index-page.css +7 -0
  172. package/src/index.js +9 -0
  173. package/src/utils/aiFeatures.js +241 -0
  174. package/src/utils/aiSuggestionTracker.js +178 -0
  175. package/src/utils/libraryWidgetKeys.js +2 -0
  176. package/src/utils/objectPermissions.js +548 -0
  177. package/src/utils/objectTranslations.js +176 -0
  178. package/src/utils/stateMachineValidation.js +250 -0
  179. package/src/utils/widgetDispatch.js +3 -1
@@ -0,0 +1,416 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+
5
+ CnAiFeaturesWidget — the hermiq AI features a record's type switches on for
6
+ a surface: where each one runs (hermiq's answer; a feature hermiq does not
7
+ offer reads unavailable, never local), a run on one of the record's
8
+ documents with a refusal shown in words naming the feature, the reason and
9
+ hermiq's gate, optional question prompts, and a use / adapt / reject
10
+ decision on every suggestion for the AI oversight trail.
11
+ -->
12
+ <template>
13
+ <div class="cn-ai-features" data-testid="cn-ai-features">
14
+ <h3 v-if="showTitle">
15
+ {{ title || t('nextcloud-vue', 'Assistant') }}
16
+ </h3>
17
+
18
+ <div v-if="loading" class="cn-ai-features__loading">
19
+ <NcLoadingIcon :size="20" />
20
+ {{ t('nextcloud-vue', 'Reading the AI features on this record') }}
21
+ </div>
22
+
23
+ <template v-else>
24
+ <section v-if="prompts && prompts.length > 0"
25
+ class="cn-ai-features__questions"
26
+ data-testid="cn-ai-questions">
27
+ <h4>{{ t('nextcloud-vue', 'Questions') }}</h4>
28
+ <ul class="cn-ai-features__prompts">
29
+ <li v-for="prompt in prompts" :key="prompt.id || prompt.label">
30
+ <NcButton :disabled="asking"
31
+ :data-testid="`cn-ai-question-${prompt.id}`"
32
+ @click="ask(prompt)">
33
+ {{ prompt.label }}
34
+ </NcButton>
35
+ </li>
36
+ </ul>
37
+ <div v-if="answer" class="cn-ai-features__output" data-testid="cn-ai-question-answer">
38
+ {{ answer }}
39
+ </div>
40
+ <CnAiSuggestionDecision v-if="answer"
41
+ :text="answer"
42
+ testid="cn-ai-question-decision"
43
+ @decide="decide('question', $event)" />
44
+ <p v-if="answerFailed" class="cn-ai-features__unreadable" data-testid="cn-ai-question-failed">
45
+ {{ answerFailed }}
46
+ </p>
47
+ </section>
48
+
49
+ <p v-if="features === null" class="cn-ai-features__unreadable" data-testid="cn-ai-features-unreadable">
50
+ {{ t('nextcloud-vue', 'The AI features on this record could not be read. This panel is incomplete, not empty.') }}
51
+ </p>
52
+
53
+ <p v-else-if="features.length === 0" class="cn-ai-features__hint" data-testid="cn-ai-features-empty">
54
+ {{ t('nextcloud-vue', 'The type of this record switches on no AI feature here.') }}
55
+ </p>
56
+
57
+ <template v-else>
58
+ <section v-for="feature in features"
59
+ :key="feature.slug"
60
+ class="cn-ai-features__feature"
61
+ :data-testid="`cn-ai-feature-${feature.slug}`">
62
+ <h4>{{ feature.slug }}</h4>
63
+ <p class="cn-ai-features__hint" :data-testid="`cn-ai-feature-where-${feature.slug}`">
64
+ {{ whereItRuns(feature) }}
65
+ </p>
66
+
67
+ <form v-if="feature.available === true"
68
+ class="cn-ai-features__run"
69
+ @submit.prevent="run(feature)">
70
+ <NcSelect :inputId="fieldId(feature, 'document')"
71
+ :inputLabel="t('nextcloud-vue', 'Document')"
72
+ :placeholder="t('nextcloud-vue', 'Choose a document')"
73
+ :options="documentOptions"
74
+ :modelValue="selectedDocument(feature)"
75
+ :data-testid="`cn-ai-feature-document-${feature.slug}`"
76
+ @update:modelValue="chooseDocument(feature, $event)" />
77
+ <p v-if="documents === null" class="cn-ai-features__unreadable">
78
+ {{ t('nextcloud-vue', 'The documents on this record could not be read.') }}
79
+ </p>
80
+
81
+ <label :for="fieldId(feature, 'instruction')">
82
+ {{ t('nextcloud-vue', 'What should it do with the document?') }}
83
+ </label>
84
+ <textarea :id="fieldId(feature, 'instruction')"
85
+ v-model="runs[feature.slug].instruction"
86
+ rows="2" />
87
+
88
+ <NcButton type="submit"
89
+ :disabled="runs[feature.slug].busy"
90
+ :data-testid="`cn-ai-feature-run-${feature.slug}`">
91
+ {{ t('nextcloud-vue', 'Run') }}
92
+ </NcButton>
93
+ </form>
94
+
95
+ <NcNoteCard v-if="runs[feature.slug] && runs[feature.slug].refusal"
96
+ type="warning"
97
+ :data-testid="`cn-ai-feature-refusal-${feature.slug}`">
98
+ <p>{{ runs[feature.slug].refusal.sentence }}</p>
99
+ <p v-if="runs[feature.slug].refusal.gate" class="cn-ai-features__gate">
100
+ {{ t('nextcloud-vue', 'Gate: {gate}', { gate: runs[feature.slug].refusal.gate }) }}
101
+ </p>
102
+ </NcNoteCard>
103
+
104
+ <p v-if="runs[feature.slug] && runs[feature.slug].failed"
105
+ class="cn-ai-features__unreadable"
106
+ :data-testid="`cn-ai-feature-failed-${feature.slug}`">
107
+ {{ t('nextcloud-vue', 'hermiq could not be reached. Nothing was run.') }}
108
+ </p>
109
+
110
+ <div v-if="runs[feature.slug] && runs[feature.slug].output"
111
+ class="cn-ai-features__output"
112
+ :data-testid="`cn-ai-feature-output-${feature.slug}`">
113
+ {{ runs[feature.slug].output }}
114
+ </div>
115
+ <CnAiSuggestionDecision v-if="runs[feature.slug] && runs[feature.slug].output"
116
+ :text="runs[feature.slug].output"
117
+ :testid="`cn-ai-feature-decision-${feature.slug}`"
118
+ @decide="decide(`feature:${feature.slug}`, $event)" />
119
+ </section>
120
+
121
+ <p class="cn-ai-features__hint">
122
+ {{ t('nextcloud-vue', 'The assistant does nothing without your confirmation. It sees only what you see on this record.') }}
123
+ </p>
124
+ </template>
125
+ </template>
126
+ </div>
127
+ </template>
128
+
129
+ <script>
130
+ import { translate as t } from '@nextcloud/l10n'
131
+ import { NcButton, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'
132
+ import CnAiSuggestionDecision from './CnAiSuggestionDecision.vue'
133
+ import {
134
+ AiFeatureRefusal,
135
+ askAbout,
136
+ DEFAULT_RUN_ENDPOINT,
137
+ fetchDocuments,
138
+ fetchFeatures,
139
+ fetchPrompts,
140
+ refusalSentence,
141
+ runFeatureOnDocument,
142
+ whereItRuns,
143
+ } from '../../utils/aiFeatures.js'
144
+ import { createSuggestionTracker } from '../../utils/aiSuggestionTracker.js'
145
+
146
+ export default {
147
+ name: 'CnAiFeaturesWidget',
148
+ components: { CnAiSuggestionDecision, NcButton, NcLoadingIcon, NcNoteCard, NcSelect },
149
+
150
+ props: {
151
+ /** The record id (injected by CnObjectSidebar). */
152
+ objectId: { type: String, default: '' },
153
+ /** Register slug or id (injected by CnObjectSidebar). */
154
+ register: { type: String, default: '' },
155
+ /** Schema slug or id (injected by CnObjectSidebar). */
156
+ schema: { type: String, default: '' },
157
+ /** OpenRegister API root. */
158
+ apiBase: { type: String, default: '/apps/openregister/api' },
159
+ /**
160
+ * Where the features are declared: the record's `typeField` references
161
+ * a `typeSchema` object whose `field` maps feature slug to surface;
162
+ * the slugs placed on `surface` are shown.
163
+ */
164
+ declaration: {
165
+ type: Object,
166
+ default: () => ({ typeField: '', typeSchema: '', field: 'aiFeatures', surface: '' }),
167
+ },
168
+
169
+ /** The endpoint that answers where each declared feature runs (`{features: [...]}`). */
170
+ featuresEndpoint: { type: String, default: '' },
171
+ /** Query key the declared slugs are sent under: `<key>[<slug>]=<surface>`. */
172
+ featuresQueryKey: { type: String, default: 'aiFeatures' },
173
+ /** The record's documents; empty uses OpenRegister's files of the record. */
174
+ filesEndpoint: { type: String, default: '' },
175
+ /** hermiq's run endpoint, with `{slug}`. */
176
+ runEndpoint: { type: String, default: DEFAULT_RUN_ENDPOINT },
177
+ /** Prompts offered as questions (`{prompts: [...]}`); empty offers none. */
178
+ promptsEndpoint: { type: String, default: '' },
179
+ /** Where a question is posted (`{<objectKey>: id, message}` → `{reply}`). */
180
+ askEndpoint: { type: String, default: '' },
181
+ /** The AI oversight record-action endpoint; empty reports nothing. */
182
+ decisionEndpoint: { type: String, default: '' },
183
+ /** Body key naming the record in the ask and decision bodies. */
184
+ objectKey: { type: String, default: 'objectId' },
185
+ /** Heading; empty uses "Assistant". */
186
+ title: { type: String, default: '' },
187
+ /** Whether the heading is drawn. */
188
+ showTitle: { type: Boolean, default: true },
189
+ },
190
+
191
+ data() {
192
+ return {
193
+ loading: true,
194
+ features: [],
195
+ documents: [],
196
+ runs: {},
197
+ prompts: [],
198
+ asking: false,
199
+ answer: '',
200
+ answerFailed: '',
201
+ }
202
+ },
203
+
204
+ computed: {
205
+ documentOptions() {
206
+ return (this.documents || []).map((doc) => ({ id: doc.id, label: doc.title }))
207
+ },
208
+
209
+ resolvedFilesEndpoint() {
210
+ return this.filesEndpoint
211
+ || `${this.apiBase.replace(/\/$/, '')}/objects/${this.register}/${this.schema}/${encodeURIComponent(this.objectId)}/files`
212
+ },
213
+ },
214
+
215
+ created() {
216
+ this.tracker = createSuggestionTracker({ objectId: this.objectId, objectKey: this.objectKey, endpoint: this.decisionEndpoint })
217
+ this.onPageHide = () => this.tracker.closeAll({ leaving: true })
218
+ window.addEventListener('pagehide', this.onPageHide)
219
+ },
220
+
221
+ mounted() {
222
+ this.load()
223
+ },
224
+
225
+ beforeUnmount() {
226
+ window.removeEventListener('pagehide', this.onPageHide)
227
+ this.tracker.closeAll()
228
+ },
229
+
230
+ methods: {
231
+ t,
232
+ whereItRuns,
233
+
234
+ /**
235
+ * Read the features, the prompts and (when a feature can run) the documents.
236
+ *
237
+ * @return {Promise<void>}
238
+ */
239
+ async load() {
240
+ this.loading = true
241
+ if (this.objectId === '' || !this.featuresEndpoint) {
242
+ this.features = null
243
+ this.loading = false
244
+ return
245
+ }
246
+
247
+ const [features, prompts] = await Promise.all([
248
+ fetchFeatures({
249
+ apiBase: this.apiBase.replace(/\/$/, ''),
250
+ register: this.register,
251
+ schema: this.schema,
252
+ objectId: this.objectId,
253
+ declaration: this.declaration,
254
+ featuresEndpoint: this.featuresEndpoint,
255
+ featuresQueryKey: this.featuresQueryKey,
256
+ }),
257
+ this.promptsEndpoint ? fetchPrompts(this.promptsEndpoint) : [],
258
+ ])
259
+ this.features = features
260
+ this.prompts = prompts || []
261
+
262
+ const runs = {}
263
+ for (const feature of features || []) {
264
+ runs[feature.slug] = { documentReference: '', instruction: '', busy: false, refusal: null, failed: false, output: '' }
265
+ }
266
+ this.runs = runs
267
+
268
+ const offersARun = (features || []).some((feature) => feature.available === true)
269
+ this.documents = offersARun ? await fetchDocuments(this.resolvedFilesEndpoint) : []
270
+ this.loading = false
271
+ },
272
+
273
+ selectedDocument(feature) {
274
+ const id = this.runs[feature.slug]?.documentReference
275
+ return this.documentOptions.find((option) => option.id === id) || null
276
+ },
277
+
278
+ chooseDocument(feature, option) {
279
+ this.runs[feature.slug].documentReference = option ? String(option.id) : ''
280
+ },
281
+
282
+ /**
283
+ * Ask one of the offered prompts about this record.
284
+ *
285
+ * @param {object} prompt The prompt.
286
+ * @return {Promise<void>}
287
+ */
288
+ async ask(prompt) {
289
+ this.tracker.dismiss('question', 'superseded')
290
+ this.asking = true
291
+ this.answer = ''
292
+ this.answerFailed = ''
293
+ try {
294
+ const result = await askAbout(this.askEndpoint, this.objectKey, this.objectId, prompt.prompt)
295
+ this.answer = String(result?.reply ?? '')
296
+ if (this.answer !== '') {
297
+ this.tracker.show('question', { type: 'qa', text: this.answer, prompt: prompt.prompt, model: String(result?.model ?? '') })
298
+ }
299
+ } catch (error) {
300
+ this.answerFailed = String(error?.response?.data?.error ?? '') || t('nextcloud-vue', 'The assistant did not answer.')
301
+ } finally {
302
+ this.asking = false
303
+ }
304
+ },
305
+
306
+ /**
307
+ * Report what the person did with the suggestion in a slot.
308
+ *
309
+ * @param {string} slot The slot key.
310
+ * @param {object} decision `{userAction, actualValue?}`.
311
+ * @return {Promise<void>}
312
+ */
313
+ decide(slot, decision) {
314
+ return this.tracker.decide(slot, decision.userAction, { actualValue: decision.actualValue })
315
+ },
316
+
317
+ fieldId(feature, field) {
318
+ return `cn-ai-${this.objectId || 'unknown'}-${feature.slug}-${field}`
319
+ },
320
+
321
+ /**
322
+ * Run one feature on the chosen document, and show what came back.
323
+ *
324
+ * @param {object} feature The feature.
325
+ * @return {Promise<void>}
326
+ */
327
+ async run(feature) {
328
+ const state = this.runs[feature.slug]
329
+ this.tracker.dismiss(`feature:${feature.slug}`, 'superseded')
330
+ state.busy = true
331
+ state.refusal = null
332
+ state.failed = false
333
+ state.output = ''
334
+ try {
335
+ const answer = await runFeatureOnDocument({
336
+ feature: feature.slug,
337
+ documentReference: state.documentReference,
338
+ instruction: state.instruction,
339
+ runEndpoint: this.runEndpoint,
340
+ })
341
+ state.output = String(answer?.output ?? '')
342
+ if (state.output !== '') {
343
+ this.tracker.show(`feature:${feature.slug}`, {
344
+ type: 'decision_support',
345
+ text: state.output,
346
+ prompt: state.instruction,
347
+ model: String(answer?.model ?? ''),
348
+ documentId: state.documentReference,
349
+ })
350
+ }
351
+ } catch (error) {
352
+ if (error instanceof AiFeatureRefusal) {
353
+ state.refusal = { sentence: refusalSentence(error, feature.slug), gate: error.gate }
354
+ } else {
355
+ state.failed = true
356
+ }
357
+ } finally {
358
+ state.busy = false
359
+ }
360
+ },
361
+ },
362
+ }
363
+ </script>
364
+
365
+ <style scoped>
366
+ .cn-ai-features {
367
+ padding: 12px;
368
+ }
369
+
370
+ .cn-ai-features__loading {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 8px;
374
+ }
375
+
376
+ .cn-ai-features__feature,
377
+ .cn-ai-features__questions {
378
+ margin-bottom: 16px;
379
+ padding-bottom: 12px;
380
+ border-bottom: 1px solid var(--color-border);
381
+ }
382
+
383
+ .cn-ai-features__prompts {
384
+ display: flex;
385
+ flex-wrap: wrap;
386
+ gap: 8px;
387
+ list-style: none;
388
+ margin: 0 0 8px;
389
+ padding: 0;
390
+ }
391
+
392
+ .cn-ai-features__run {
393
+ display: flex;
394
+ flex-direction: column;
395
+ gap: 4px;
396
+ margin: 8px 0;
397
+ }
398
+
399
+ .cn-ai-features__hint,
400
+ .cn-ai-features__gate {
401
+ display: block;
402
+ color: var(--color-text-maxcontrast);
403
+ font-size: 0.85em;
404
+ }
405
+
406
+ .cn-ai-features__output {
407
+ white-space: pre-wrap;
408
+ padding: 8px;
409
+ background: var(--color-background-dark);
410
+ border-radius: var(--border-radius);
411
+ }
412
+
413
+ .cn-ai-features__unreadable {
414
+ color: var(--color-warning-text, var(--color-main-text));
415
+ }
416
+ </style>
@@ -0,0 +1,136 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+
5
+ What the person does with one AI suggestion: use it, adapt it, or reject it.
6
+ Using it copies it to the clipboard. Emits `decide` with
7
+ `{ userAction, actualValue? }` for the oversight trail.
8
+ -->
9
+ <template>
10
+ <div class="cn-ai-suggestion-decision" :data-testid="testid">
11
+ <p v-if="decided"
12
+ class="cn-ai-suggestion-decision__done"
13
+ role="status"
14
+ :data-testid="`${testid}-done`">
15
+ {{ decided }}
16
+ </p>
17
+ <template v-else>
18
+ <template v-if="editing">
19
+ <label :for="`${testid}-draft`">
20
+ {{ t('nextcloud-vue', 'Your version') }}
21
+ </label>
22
+ <textarea :id="`${testid}-draft`"
23
+ v-model="draft"
24
+ rows="4"
25
+ :data-testid="`${testid}-draft`" />
26
+ </template>
27
+ <div class="cn-ai-suggestion-decision__actions">
28
+ <NcButton v-if="editing"
29
+ variant="primary"
30
+ :data-testid="`${testid}-use-adapted`"
31
+ @click="decide('modified')">
32
+ {{ t('nextcloud-vue', 'Use my version') }}
33
+ </NcButton>
34
+ <template v-else>
35
+ <NcButton variant="primary"
36
+ :data-testid="`${testid}-use`"
37
+ @click="decide('accepted')">
38
+ {{ t('nextcloud-vue', 'Use this') }}
39
+ </NcButton>
40
+ <NcButton :data-testid="`${testid}-adapt`" @click="startEditing">
41
+ {{ t('nextcloud-vue', 'Adapt') }}
42
+ </NcButton>
43
+ </template>
44
+ <NcButton variant="tertiary"
45
+ :data-testid="`${testid}-reject`"
46
+ @click="decide('rejected')">
47
+ {{ t('nextcloud-vue', 'Reject') }}
48
+ </NcButton>
49
+ </div>
50
+ </template>
51
+ </div>
52
+ </template>
53
+
54
+ <script>
55
+ import { translate as t } from '@nextcloud/l10n'
56
+ import { NcButton } from '@nextcloud/vue'
57
+
58
+ export default {
59
+ name: 'CnAiSuggestionDecision',
60
+ components: { NcButton },
61
+
62
+ props: {
63
+ /** The suggestion text. */
64
+ text: { type: String, required: true },
65
+ /** Test id prefix, also the base of the draft field id. */
66
+ testid: { type: String, default: 'cn-ai-suggestion-decision' },
67
+ },
68
+
69
+ emits: ['decide'],
70
+
71
+ data() {
72
+ return { editing: false, draft: '', decided: '' }
73
+ },
74
+
75
+ watch: {
76
+ text() {
77
+ this.editing = false
78
+ this.draft = ''
79
+ this.decided = ''
80
+ },
81
+ },
82
+
83
+ methods: {
84
+ t,
85
+
86
+ startEditing() {
87
+ this.draft = this.text
88
+ this.editing = true
89
+ },
90
+
91
+ /**
92
+ * Report the decision, and copy a used suggestion to the clipboard.
93
+ *
94
+ * @param {string} userAction accepted, modified or rejected.
95
+ * @return {Promise<void>}
96
+ */
97
+ async decide(userAction) {
98
+ const used = userAction === 'modified' ? this.draft : this.text
99
+ this.$emit('decide', { userAction, actualValue: userAction === 'modified' ? this.draft : undefined })
100
+ this.decided = {
101
+ accepted: t('nextcloud-vue', 'Used. Copied to your clipboard.'),
102
+ modified: t('nextcloud-vue', 'Your version is used. Copied to your clipboard.'),
103
+ rejected: t('nextcloud-vue', 'Rejected.'),
104
+ }[userAction]
105
+ if (userAction === 'rejected') {
106
+ return
107
+ }
108
+ try {
109
+ await navigator.clipboard.writeText(used)
110
+ } catch {
111
+ this.decided = userAction === 'modified' ? t('nextcloud-vue', 'Your version is used.') : t('nextcloud-vue', 'Used.')
112
+ }
113
+ },
114
+ },
115
+ }
116
+ </script>
117
+
118
+ <style scoped>
119
+ .cn-ai-suggestion-decision {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 4px;
123
+ margin-top: 8px;
124
+ }
125
+
126
+ .cn-ai-suggestion-decision__actions {
127
+ display: flex;
128
+ flex-wrap: wrap;
129
+ gap: 8px;
130
+ }
131
+
132
+ .cn-ai-suggestion-decision__done {
133
+ color: var(--color-text-maxcontrast);
134
+ font-size: 0.85em;
135
+ }
136
+ </style>