@conduction/nextcloud-vue 2.119.0 → 2.120.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.
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +166 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +166 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +889 -101
- package/dist/esm/utils/aiFeatures.js +245 -0
- package/dist/esm/utils/aiFeatures.js.map +1 -0
- package/dist/esm/utils/aiSuggestionTracker.js +182 -0
- package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectPermissions.js +552 -0
- package/dist/esm/utils/objectPermissions.js.map +1 -0
- package/dist/esm/utils/objectTranslations.js +180 -0
- package/dist/esm/utils/objectTranslations.js.map +1 -0
- package/dist/esm/utils/stateMachineValidation.js +254 -0
- package/dist/esm/utils/stateMachineValidation.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +3 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +20052 -13138
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +889 -101
- package/l10n/en.json +166 -1
- package/l10n/nl.json +166 -1
- package/package.json +1 -1
- package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
- package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
- package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
- package/src/components/CnAiFeaturesWidget/index.js +14 -0
- package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
- package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
- package/src/components/CnObjectPermissionsTab/index.js +13 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
- package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
- package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
- package/src/components/CnObjectTranslationsWidget/index.js +13 -0
- package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
- package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
- package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
- package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
- package/src/components/CnStateMachineEditor/index.js +8 -0
- package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
- package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
- package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/src/components/index.js +4 -0
- package/src/index.js +5 -0
- package/src/utils/aiFeatures.js +241 -0
- package/src/utils/aiSuggestionTracker.js +178 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/objectPermissions.js +548 -0
- package/src/utils/objectTranslations.js +176 -0
- package/src/utils/stateMachineValidation.js +250 -0
- package/src/utils/widgetDispatch.js +3 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
2
|
<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->
|
|
3
3
|
<template>
|
|
4
|
-
<
|
|
4
|
+
<CnTimelineManage v-if="manage"
|
|
5
|
+
:register="resolvedRegister"
|
|
6
|
+
:schema="resolvedSchema"
|
|
7
|
+
:objectId="resolvedObjectId"
|
|
8
|
+
:apiBase="apiBase"
|
|
9
|
+
:content="content" />
|
|
10
|
+
<section v-else
|
|
11
|
+
class="cn-timeline-widget"
|
|
12
|
+
data-testid="cn-timeline-widget"
|
|
13
|
+
:aria-busy="loading ? 'true' : 'false'">
|
|
5
14
|
<h3 v-if="resolvedTitle" class="cn-timeline-widget__title">
|
|
6
15
|
{{ resolvedTitle }}
|
|
7
16
|
</h3>
|
|
@@ -53,6 +62,7 @@
|
|
|
53
62
|
<script>
|
|
54
63
|
import { getLanguage, translate as t } from '@nextcloud/l10n'
|
|
55
64
|
import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
65
|
+
import CnTimelineManage from './CnTimelineManage.vue'
|
|
56
66
|
import { auditEvents, fieldEvents, listEvents, relatedEvents, sortEvents, timelineEntryEvents } from '../../utils/timelineEvents.js'
|
|
57
67
|
import { userLocale } from '../../utils/userLocale.js'
|
|
58
68
|
|
|
@@ -74,6 +84,12 @@ import { userLocale } from '../../utils/userLocale.js'
|
|
|
74
84
|
* Oldest first by default (`order: 'desc'` flips it). A moment after now is
|
|
75
85
|
* marked upcoming, in words as well as style.
|
|
76
86
|
*
|
|
87
|
+
* `manage: true` swaps the read view for the manage view (CnTimelineManage)
|
|
88
|
+
* over OpenRegister's timeline alone: kind chips, a visibility filter, pin,
|
|
89
|
+
* follow-up, a note composer with standard text blocks, CSV export, and
|
|
90
|
+
* `entryActions` that open a registry modal on an entry. The manage controls
|
|
91
|
+
* are drawn only when the timeline read answers `canManage: true`.
|
|
92
|
+
*
|
|
77
93
|
* ```json
|
|
78
94
|
* { "type": "timeline", "content": {
|
|
79
95
|
* "title": "Timeline",
|
|
@@ -95,6 +111,7 @@ export default {
|
|
|
95
111
|
name: 'CnTimelineWidget',
|
|
96
112
|
|
|
97
113
|
components: {
|
|
114
|
+
CnTimelineManage,
|
|
98
115
|
NcLoadingIcon,
|
|
99
116
|
NcNoteCard,
|
|
100
117
|
},
|
|
@@ -123,7 +140,7 @@ export default {
|
|
|
123
140
|
object: { type: Object, default: null },
|
|
124
141
|
/** Card title override. */
|
|
125
142
|
title: { type: String, default: '' },
|
|
126
|
-
/** Stored widget content: `{ title, fields, related, auditTrail, timeline, order }`. */
|
|
143
|
+
/** Stored widget content: `{ title, fields, related, auditTrail, timeline, order, manage, entryActions, ... }`. */
|
|
127
144
|
content: { type: Object, default: () => ({}) },
|
|
128
145
|
/** OpenRegister API base. */
|
|
129
146
|
apiBase: { type: String, default: '/apps/openregister/api' },
|
|
@@ -172,6 +189,15 @@ export default {
|
|
|
172
189
|
return this.object || this.ctx.object || this.ctx.objectData || this.fetchedObject || null
|
|
173
190
|
},
|
|
174
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Whether the manage view renders instead of the read view.
|
|
194
|
+
*
|
|
195
|
+
* @return {boolean}
|
|
196
|
+
*/
|
|
197
|
+
manage() {
|
|
198
|
+
return this.content.manage === true
|
|
199
|
+
},
|
|
200
|
+
|
|
175
201
|
resolvedTitle() {
|
|
176
202
|
const raw = this.title || this.content.title || ''
|
|
177
203
|
return raw ? this.translate(raw) : ''
|
|
@@ -313,7 +339,7 @@ export default {
|
|
|
313
339
|
const register = this.resolvedRegister
|
|
314
340
|
const schema = this.resolvedSchema
|
|
315
341
|
const seq = ++this.loadSeq
|
|
316
|
-
if (!id || !register || !schema) {
|
|
342
|
+
if (!id || !register || !schema || this.manage) {
|
|
317
343
|
return
|
|
318
344
|
}
|
|
319
345
|
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
@@ -26,6 +26,7 @@ import CnNoticeWidget from '../CnNoticeWidget/CnNoticeWidget.vue'
|
|
|
26
26
|
import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
|
|
27
27
|
import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
|
|
28
28
|
import CnObjectMetadataWidget from '../CnObjectMetadataWidget/CnObjectMetadataWidget.vue'
|
|
29
|
+
import CnObjectTranslationsWidget from '../CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue'
|
|
29
30
|
import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
|
|
30
31
|
import CnSavedViewAlertWidget from '../CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue'
|
|
31
32
|
import CnTextWidget from '../CnTextWidget/CnTextWidget.vue'
|
|
@@ -74,6 +75,10 @@ import CnWidgetObjectTable from '../CnWidgetObjectTable/CnWidgetObjectTable.vue'
|
|
|
74
75
|
* data — the intended remedy for a deep menu group collapsed into one
|
|
75
76
|
* top-level entry via `menu-layout.json` relocations.
|
|
76
77
|
*
|
|
78
|
+
* The `object-translations` key places CnObjectTranslationsWidget: the
|
|
79
|
+
* object's translatable properties per register language, with completeness
|
|
80
|
+
* chips and one save (register / schema / objectId via the detail-context merge).
|
|
81
|
+
*
|
|
77
82
|
* @type {Record<string, import('vue').Component>}
|
|
78
83
|
*/
|
|
79
84
|
export const BUILT_IN_WIDGETS = {
|
|
@@ -97,4 +102,5 @@ export const BUILT_IN_WIDGETS = {
|
|
|
97
102
|
divider: CnDividerWidget,
|
|
98
103
|
'saved-view-alert': CnSavedViewAlertWidget,
|
|
99
104
|
exports: CnExportsWidget,
|
|
105
|
+
'object-translations': CnObjectTranslationsWidget,
|
|
100
106
|
}
|
package/src/components/index.js
CHANGED
|
@@ -77,6 +77,7 @@ export { CnRegisterSchemaSelect } from './CnRegisterSchemaSelect/index.js'
|
|
|
77
77
|
export { CnThemePreview } from './CnThemePreview/index.js'
|
|
78
78
|
export { CnRelationshipGraph } from './CnRelationshipGraph/index.js'
|
|
79
79
|
export { CnGraphCanvas } from './CnGraphCanvas/index.js'
|
|
80
|
+
export { CnStateMachineEditor } from './CnStateMachineEditor/index.js'
|
|
80
81
|
export { CnFlowDetail, CnFlowIndexPage, CnFlowSidebar } from './CnFlowDetail/index.js'
|
|
81
82
|
export { CnIcon, ICON_MAP, registerIcons } from './CnIcon/index.js'
|
|
82
83
|
export {
|
|
@@ -167,7 +168,10 @@ export { CnJourneyDialog } from './CnJourneyDialog/index.js'
|
|
|
167
168
|
export { CnProcessSteps } from './CnProcessSteps/index.js'
|
|
168
169
|
export { CnSubObjectsField } from './CnSubObjectsField/index.js'
|
|
169
170
|
export { CnIntegrationTab } from './CnIntegrationTab/index.js'
|
|
171
|
+
export { CnAiFeaturesWidget } from './CnAiFeaturesWidget/index.js'
|
|
170
172
|
export { CnObjectAccessTab } from './CnObjectAccessTab/index.js'
|
|
173
|
+
export { CnObjectPermissionsTab } from './CnObjectPermissionsTab/index.js'
|
|
174
|
+
export { CnObjectTranslationsWidget } from './CnObjectTranslationsWidget/index.js'
|
|
171
175
|
export { CnIntegrationCard } from './CnIntegrationCard/index.js'
|
|
172
176
|
export { CnIntegrationWidgetGrid } from './CnIntegrationWidgetGrid/index.js'
|
|
173
177
|
export { CnIntegrationWidget } from './CnIntegrationWidget/index.js'
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
CnAdminActionCard,
|
|
41
41
|
CnAdminSettingsShell,
|
|
42
42
|
CnAdvancedFormDialog,
|
|
43
|
+
CnAiFeaturesWidget,
|
|
43
44
|
CnApiReference,
|
|
44
45
|
CnAppLoading,
|
|
45
46
|
CnAppNav,
|
|
@@ -249,9 +250,11 @@ export {
|
|
|
249
250
|
CnObjectMetadataModal,
|
|
250
251
|
CnObjectMetadataWidget,
|
|
251
252
|
CnObjectPartiesWidget,
|
|
253
|
+
CnObjectPermissionsTab,
|
|
252
254
|
CnObjectPresenceWidget,
|
|
253
255
|
CnObjectRow,
|
|
254
256
|
CnObjectSidebar,
|
|
257
|
+
CnObjectTranslationsWidget,
|
|
255
258
|
CnOfflineQueue,
|
|
256
259
|
CnOrderedListField,
|
|
257
260
|
CnPageHeader,
|
|
@@ -317,6 +320,7 @@ export {
|
|
|
317
320
|
CnSpendAnalyticsWidgetForm,
|
|
318
321
|
CnStagesWidget,
|
|
319
322
|
CnStagesWidgetForm,
|
|
323
|
+
CnStateMachineEditor,
|
|
320
324
|
CnStatsBlock,
|
|
321
325
|
CnStatsBlockWidget,
|
|
322
326
|
CnStatsBlockWidgetForm,
|
|
@@ -618,6 +622,7 @@ export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData
|
|
|
618
622
|
// drift on what a 409 means (breaking change / schema still has objects).
|
|
619
623
|
export { deleteSchema, describeSchemaChange, saveSchema, SchemaBreakingChangeError, SchemaHasObjectsError } from './utils/index.js'
|
|
620
624
|
export { validateManifest, validateManifestV2 } from './utils/validateManifest.js'
|
|
625
|
+
export { validateStateMachine } from './utils/stateMachineValidation.js'
|
|
621
626
|
export { clearResolveCache, resolveManifestSentinels } from './utils/resolveManifestSentinels.js'
|
|
622
627
|
export { clearRouteSentinelWarnings, resolveRouteSentinels } from './utils/resolveRouteSentinels.js'
|
|
623
628
|
export {
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The reads and the run behind CnAiFeaturesWidget: the AI features a record's
|
|
6
|
+
* TYPE object places on a surface, where hermiq runs each one, a run on one of
|
|
7
|
+
* the record's documents, and the sentence a refusal is shown as.
|
|
8
|
+
*
|
|
9
|
+
* An undeclared type asks nobody: the declaration is read first and the
|
|
10
|
+
* features endpoint is called only when something is placed on the surface.
|
|
11
|
+
* Every read answers null when it failed, so the widget can say it is
|
|
12
|
+
* incomplete instead of drawing an empty list.
|
|
13
|
+
*
|
|
14
|
+
* The run goes to hermiq in the reader's own session, so their own access
|
|
15
|
+
* decides which document can be read. A request without a usable document
|
|
16
|
+
* reference is refused before it is sent: hermiq's redaction gate sits on the
|
|
17
|
+
* document, and a request without one would run unchecked.
|
|
18
|
+
*/
|
|
19
|
+
import axios from '@nextcloud/axios'
|
|
20
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
21
|
+
import { prefixUrl } from './headers.js'
|
|
22
|
+
|
|
23
|
+
/** The gate name for a run refused before it was sent. */
|
|
24
|
+
export const MISSING_REFERENCE = 'missing-reference'
|
|
25
|
+
|
|
26
|
+
/** The hermiq endpoint a feature runs on a document through; `{slug}` is filled in. */
|
|
27
|
+
export const DEFAULT_RUN_ENDPOINT = '/apps/hermiq/api/ai-features/{slug}/run-on-document'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A run that was refused, before sending or by one of hermiq's gates.
|
|
31
|
+
*/
|
|
32
|
+
export class AiFeatureRefusal extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} feature The feature slug.
|
|
35
|
+
* @param {string} reason The refusing side's own sentence.
|
|
36
|
+
* @param {string|null} gate The gate that refused, when named.
|
|
37
|
+
*/
|
|
38
|
+
constructor(feature, reason, gate) {
|
|
39
|
+
super(reason)
|
|
40
|
+
this.name = 'AiFeatureRefusal'
|
|
41
|
+
this.feature = feature
|
|
42
|
+
this.reason = reason
|
|
43
|
+
this.gate = gate
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The id inside a reference that may be a plain id or an object carrying one.
|
|
49
|
+
*
|
|
50
|
+
* @param {string|object|null|undefined} value The reference.
|
|
51
|
+
* @return {string} The id, or ''.
|
|
52
|
+
*/
|
|
53
|
+
export function referenceId(value) {
|
|
54
|
+
if (value !== null && typeof value === 'object') {
|
|
55
|
+
return String(value.id ?? value.uuid ?? '').trim()
|
|
56
|
+
}
|
|
57
|
+
return String(value ?? '').trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The slugs a type object places on one surface. The declaration is a map of
|
|
62
|
+
* feature slug to surface name.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} typeObject The type object.
|
|
65
|
+
* @param {string} field The declaration field.
|
|
66
|
+
* @param {string} surface The surface.
|
|
67
|
+
* @return {Array<string>} The slugs, in declaration order.
|
|
68
|
+
*/
|
|
69
|
+
export function surfaceSlugs(typeObject, field, surface) {
|
|
70
|
+
const declared = typeObject?.[field]
|
|
71
|
+
if (declared === null || typeof declared !== 'object' || Array.isArray(declared)) {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
return Object.keys(declared).filter((slug) => declared[slug] === surface)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read one OpenRegister object.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} apiBase API root.
|
|
81
|
+
* @param {string} register Register.
|
|
82
|
+
* @param {string} schema Schema.
|
|
83
|
+
* @param {string} id Object id.
|
|
84
|
+
* @return {Promise<object>} The object.
|
|
85
|
+
*/
|
|
86
|
+
async function readObject(apiBase, register, schema, id) {
|
|
87
|
+
const { data } = await axios.get(prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`))
|
|
88
|
+
return data?.object ?? data ?? {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The AI features the record's type places on the surface, each with where it
|
|
93
|
+
* runs as the features endpoint answers it.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} options The options.
|
|
96
|
+
* @param {string} options.apiBase OpenRegister API root.
|
|
97
|
+
* @param {string} options.register Register.
|
|
98
|
+
* @param {string} options.schema Schema of the record.
|
|
99
|
+
* @param {string} options.objectId The record id.
|
|
100
|
+
* @param {object} options.declaration `{typeField, typeSchema, field, surface}`.
|
|
101
|
+
* @param {string} options.featuresEndpoint The endpoint that asks hermiq.
|
|
102
|
+
* @param {string} options.featuresQueryKey Query key the slugs are sent under.
|
|
103
|
+
* @return {Promise<Array<object>|null>} The features, or null when they could not be read.
|
|
104
|
+
*/
|
|
105
|
+
export async function fetchFeatures({ apiBase, register, schema, objectId, declaration, featuresEndpoint, featuresQueryKey }) {
|
|
106
|
+
try {
|
|
107
|
+
const record = await readObject(apiBase, register, schema, objectId)
|
|
108
|
+
const typeId = referenceId(record[declaration.typeField])
|
|
109
|
+
if (typeId === '') {
|
|
110
|
+
return []
|
|
111
|
+
}
|
|
112
|
+
const typeObject = await readObject(apiBase, register, declaration.typeSchema, typeId)
|
|
113
|
+
const slugs = surfaceSlugs(typeObject, declaration.field || 'aiFeatures', declaration.surface)
|
|
114
|
+
if (slugs.length === 0) {
|
|
115
|
+
return []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const query = new URLSearchParams({ surface: declaration.surface })
|
|
119
|
+
for (const slug of slugs) {
|
|
120
|
+
query.append(`${featuresQueryKey}[${slug}]`, declaration.surface)
|
|
121
|
+
}
|
|
122
|
+
const { data } = await axios.get(`${prefixUrl(featuresEndpoint)}?${query.toString()}`)
|
|
123
|
+
return Array.isArray(data?.features) ? data.features : []
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The record's documents a feature can be run on.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} url The files endpoint.
|
|
133
|
+
* @return {Promise<Array<{id: string, title: string}>|null>} The documents, or null.
|
|
134
|
+
*/
|
|
135
|
+
export async function fetchDocuments(url) {
|
|
136
|
+
try {
|
|
137
|
+
const { data } = await axios.get(prefixUrl(url))
|
|
138
|
+
const rows = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : [])
|
|
139
|
+
return rows
|
|
140
|
+
.filter((row) => row && row.id !== undefined && row.id !== null)
|
|
141
|
+
.map((row) => ({ id: String(row.id), title: String(row.title ?? row.name ?? row.id) }))
|
|
142
|
+
} catch {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The prompts offered on the record, as the endpoint holds them.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} url The prompts endpoint.
|
|
151
|
+
* @return {Promise<Array<{id: string, label: string, prompt: string}>|null>} The prompts, or null.
|
|
152
|
+
*/
|
|
153
|
+
export async function fetchPrompts(url) {
|
|
154
|
+
try {
|
|
155
|
+
const { data } = await axios.get(prefixUrl(url))
|
|
156
|
+
return Array.isArray(data?.prompts) ? data.prompts : []
|
|
157
|
+
} catch {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ask one prompt about the record.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} url The ask endpoint.
|
|
166
|
+
* @param {string} objectKey Body key naming the record.
|
|
167
|
+
* @param {string} objectId The record id.
|
|
168
|
+
* @param {string} message The prompt.
|
|
169
|
+
* @return {Promise<object>} `{reply, model?}`.
|
|
170
|
+
*/
|
|
171
|
+
export async function askAbout(url, objectKey, objectId, message) {
|
|
172
|
+
const { data } = await axios.post(prefixUrl(url), { [objectKey]: objectId, message })
|
|
173
|
+
return data
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Where a feature runs, in words. A feature hermiq does not offer reads
|
|
178
|
+
* unavailable, never local.
|
|
179
|
+
*
|
|
180
|
+
* @param {object} feature One feature row.
|
|
181
|
+
* @return {string} The sentence.
|
|
182
|
+
*/
|
|
183
|
+
export function whereItRuns(feature) {
|
|
184
|
+
if (feature?.available !== true) {
|
|
185
|
+
return t('nextcloud-vue', 'Unavailable: hermiq does not offer this feature here.')
|
|
186
|
+
}
|
|
187
|
+
const provider = String(feature.provider ?? '').trim()
|
|
188
|
+
const residency = String(feature.residency ?? '').trim()
|
|
189
|
+
if (provider === '' || residency === '') {
|
|
190
|
+
return t('nextcloud-vue', 'hermiq did not say which provider runs this, or where.')
|
|
191
|
+
}
|
|
192
|
+
return t('nextcloud-vue', '{provider}, runs in {residency}', { provider, residency })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run one AI feature on one document, through hermiq.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} request The run.
|
|
199
|
+
* @param {string} request.feature The feature slug.
|
|
200
|
+
* @param {string|number} request.documentReference The document's file id.
|
|
201
|
+
* @param {string} request.instruction What to do with it.
|
|
202
|
+
* @param {string} [request.runEndpoint] Endpoint template with `{slug}`.
|
|
203
|
+
* @return {Promise<object>} hermiq's answer.
|
|
204
|
+
* @throws {AiFeatureRefusal} When the reference is missing, or hermiq refuses.
|
|
205
|
+
*/
|
|
206
|
+
export async function runFeatureOnDocument({ feature, documentReference, instruction, runEndpoint = DEFAULT_RUN_ENDPOINT }) {
|
|
207
|
+
const reference = String(documentReference ?? '').trim()
|
|
208
|
+
if (/^\d+$/.test(reference) === false) {
|
|
209
|
+
throw new AiFeatureRefusal(feature, t('nextcloud-vue', 'No document was chosen.'), MISSING_REFERENCE)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const url = prefixUrl(runEndpoint.replace('{slug}', encodeURIComponent(feature)))
|
|
213
|
+
try {
|
|
214
|
+
const response = await axios.post(url, { documentReference: reference, instruction })
|
|
215
|
+
return response.data
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const status = error?.response?.status
|
|
218
|
+
const data = error?.response?.data ?? {}
|
|
219
|
+
if ([400, 403, 404, 422].includes(status) === true) {
|
|
220
|
+
throw new AiFeatureRefusal(feature, String(data.error ?? ''), data.gate ?? null)
|
|
221
|
+
}
|
|
222
|
+
throw error
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The sentence a reader sees for a refusal, naming the feature and the reason.
|
|
228
|
+
*
|
|
229
|
+
* @param {AiFeatureRefusal} refusal The refusal.
|
|
230
|
+
* @param {string} featureLabel The feature's name.
|
|
231
|
+
* @return {string} The sentence.
|
|
232
|
+
*/
|
|
233
|
+
export function refusalSentence(refusal, featureLabel) {
|
|
234
|
+
if (refusal.gate === MISSING_REFERENCE) {
|
|
235
|
+
return t('nextcloud-vue', '{feature} reads a document, and no document was chosen.', { feature: featureLabel })
|
|
236
|
+
}
|
|
237
|
+
if (refusal.gate === 'redaction') {
|
|
238
|
+
return t('nextcloud-vue', '{feature} will not read a document that has not been redacted.', { feature: featureLabel })
|
|
239
|
+
}
|
|
240
|
+
return t('nextcloud-vue', '{feature} did not run: {reason}', { feature: featureLabel, reason: refusal.reason })
|
|
241
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Reports what a person did with each AI suggestion on a record, for the AI
|
|
6
|
+
* oversight trail (EU AI Act art. 14). Every suggestion that is shown ends in
|
|
7
|
+
* exactly one report: accepted, modified or rejected when the person acts on
|
|
8
|
+
* it, and ignored when they do not. An ignored suggestion says why: a newer
|
|
9
|
+
* suggestion replaced it (superseded), its surface was closed (closed), or it
|
|
10
|
+
* stayed open past its lifetime (expired).
|
|
11
|
+
*
|
|
12
|
+
* The app's record-action endpoint writes the local audit entry and hands the
|
|
13
|
+
* decision to hermiq; without hermiq it keeps only the local entry.
|
|
14
|
+
*/
|
|
15
|
+
import { getRequestToken } from '@nextcloud/auth'
|
|
16
|
+
import axios from '@nextcloud/axios'
|
|
17
|
+
import { prefixUrl } from './headers.js'
|
|
18
|
+
|
|
19
|
+
/** How long a shown suggestion waits for a decision before it expires. */
|
|
20
|
+
export const SUGGESTION_LIFETIME_MS = 30 * 60 * 1000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Post one decision to the record-action endpoint.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} endpoint The record-action endpoint.
|
|
26
|
+
* @param {object} body The record-action body.
|
|
27
|
+
* @param {object} options Options.
|
|
28
|
+
* @param {boolean} options.leaving True while the page unloads: the request
|
|
29
|
+
* then uses `fetch` with `keepalive`, which outlives the page.
|
|
30
|
+
* @return {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
export async function postDecision(endpoint, body, { leaving = false } = {}) {
|
|
33
|
+
const url = prefixUrl(endpoint)
|
|
34
|
+
if (leaving === true) {
|
|
35
|
+
await fetch(url, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
keepalive: true,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
requesttoken: getRequestToken() ?? '',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
})
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await axios.post(url, body)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Track the suggestions shown on one record.
|
|
52
|
+
*
|
|
53
|
+
* A suggestion is shown under a slot key (one feature, or the questions); a
|
|
54
|
+
* newer suggestion in the same slot supersedes an undecided older one.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} options Options.
|
|
57
|
+
* @param {string} options.objectId The record id.
|
|
58
|
+
* @param {string} [options.objectKey] Body key naming the record (default `objectId`).
|
|
59
|
+
* @param {string} [options.endpoint] The record-action endpoint; empty reports nothing.
|
|
60
|
+
* @param {function(object, object): Promise<void>} [options.report] Sends one decision; defaults to posting to `endpoint`.
|
|
61
|
+
* @param {number} [options.lifetimeMs] Lifetime of an undecided suggestion.
|
|
62
|
+
* @param {function(function(): void, number): number} [options.setTimer] Timer factory, for tests.
|
|
63
|
+
* @param {function(number): void} [options.clearTimer] Timer cancel, for tests.
|
|
64
|
+
* @return {object} The tracker: show, decide, dismiss, closeAll, pending.
|
|
65
|
+
*/
|
|
66
|
+
export function createSuggestionTracker({
|
|
67
|
+
objectId,
|
|
68
|
+
objectKey = 'objectId',
|
|
69
|
+
endpoint = '',
|
|
70
|
+
report = (body, options) => (endpoint ? postDecision(endpoint, body, options) : Promise.resolve()),
|
|
71
|
+
lifetimeMs = SUGGESTION_LIFETIME_MS,
|
|
72
|
+
setTimer = setTimeout,
|
|
73
|
+
clearTimer = clearTimeout,
|
|
74
|
+
}) {
|
|
75
|
+
const pending = new Map()
|
|
76
|
+
let counter = 0
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Send the report for one slot's suggestion and forget it.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} slot The slot key.
|
|
82
|
+
* @param {object} decision `{userAction, unusedBecause?, actualValue?, reason?}`.
|
|
83
|
+
* @param {object} options Passed to report.
|
|
84
|
+
* @return {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
function settle(slot, decision, options = {}) {
|
|
87
|
+
const shown = pending.get(slot)
|
|
88
|
+
if (shown === undefined) {
|
|
89
|
+
return Promise.resolve()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pending.delete(slot)
|
|
93
|
+
clearTimer(shown.timer)
|
|
94
|
+
const body = {
|
|
95
|
+
[objectKey]: objectId,
|
|
96
|
+
type: shown.type,
|
|
97
|
+
suggestion: shown.text,
|
|
98
|
+
prompt: shown.prompt ?? '',
|
|
99
|
+
model: shown.model ?? '',
|
|
100
|
+
documentId: shown.documentId ?? '',
|
|
101
|
+
suggestionId: shown.id,
|
|
102
|
+
...decision,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Promise.resolve(report(body, options)).catch(() => {
|
|
106
|
+
// The person's work must not fail because the oversight trail
|
|
107
|
+
// could not be written; the server logs its own failures.
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
/**
|
|
113
|
+
* Register a suggestion as shown in a slot.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} slot The slot key.
|
|
116
|
+
* @param {object} suggestion `{type, text, prompt?, model?, documentId?}`.
|
|
117
|
+
* @return {string} The suggestion id.
|
|
118
|
+
*/
|
|
119
|
+
show(slot, suggestion) {
|
|
120
|
+
settle(slot, { userAction: 'ignored', unusedBecause: 'superseded' })
|
|
121
|
+
counter += 1
|
|
122
|
+
const id = `${Date.now().toString(36)}-${counter}`
|
|
123
|
+
const timer = setTimer(() => {
|
|
124
|
+
settle(slot, { userAction: 'ignored', unusedBecause: 'expired' })
|
|
125
|
+
}, lifetimeMs)
|
|
126
|
+
pending.set(slot, { ...suggestion, id, timer })
|
|
127
|
+
return id
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Report the person's decision on a slot's suggestion.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} slot The slot key.
|
|
134
|
+
* @param {string} userAction accepted, modified or rejected.
|
|
135
|
+
* @param {object} [extra] `{actualValue?, reason?}`.
|
|
136
|
+
* @return {Promise<void>}
|
|
137
|
+
*/
|
|
138
|
+
decide(slot, userAction, extra = {}) {
|
|
139
|
+
return settle(slot, { userAction, ...extra })
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Report a slot's undecided suggestion as ignored, for a reason.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} slot The slot key.
|
|
146
|
+
* @param {string} because closed, expired or superseded.
|
|
147
|
+
* @return {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
dismiss(slot, because) {
|
|
150
|
+
return settle(slot, { userAction: 'ignored', unusedBecause: because })
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Report every undecided suggestion as closed.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [options] `{leaving}` while the page unloads.
|
|
157
|
+
* @return {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
closeAll(options = {}) {
|
|
160
|
+
const slots = [...pending.keys()]
|
|
161
|
+
return Promise.all(slots.map((slot) => settle(
|
|
162
|
+
slot,
|
|
163
|
+
{ userAction: 'ignored', unusedBecause: 'closed' },
|
|
164
|
+
options,
|
|
165
|
+
))).then(() => undefined)
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether a slot holds an undecided suggestion.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} slot The slot key.
|
|
172
|
+
* @return {boolean} True while undecided.
|
|
173
|
+
*/
|
|
174
|
+
pending(slot) {
|
|
175
|
+
return pending.has(slot)
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|