@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.
- 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/CnIndexPage/CnIndexPage.vue2.js +53 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
- package/dist/esm/components/CnIndexPage/useNamedSource.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/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.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/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +116 -16
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +207 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +207 -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 +951 -16
- 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 +2 -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 +23038 -14567
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +951 -16
- package/l10n/en.json +207 -1
- package/l10n/nl.json +207 -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/CnIndexPage/CnIndexPage.vue +74 -3
- package/src/components/CnIndexPage/useNamedSource.js +9 -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/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
- package/src/components/CnReadinessChecklist/index.js +9 -0
- package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
- package/src/components/CnSchemaRulesWidget/index.js +10 -0
- package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
- package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
- package/src/components/CnSearchIndexStatus/index.js +10 -0
- package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
- package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
- package/src/components/CnShippedConfigurationSection/index.js +9 -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/CnWidgetGrid/registerDashboardWidgets.js +10 -0
- package/src/components/index.js +8 -0
- package/src/composables/indexSources.js +116 -16
- package/src/css/index-page.css +7 -0
- package/src/index.js +9 -0
- package/src/utils/aiFeatures.js +241 -0
- package/src/utils/aiSuggestionTracker.js +178 -0
- package/src/utils/libraryWidgetKeys.js +2 -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
|
@@ -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>
|