@aleph-alpha/chat-kit 6.5.0 → 6.7.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/{CkHistory.vue_vue_type_script_setup_true_lang-C4XS2QQI.js → CkHistory.vue_vue_type_script_setup_true_lang-DF5yUf7K.js} +619 -152
- package/dist/chat-kit.css +9 -9
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +4 -4
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +5 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +14 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +1 -1
- package/dist/components/base/CkToolApproval/ApprovalCard.vue.d.ts +21 -0
- package/dist/components/base/CkToolApproval/ApprovalCard.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/ArgumentsJson.vue.d.ts +10 -0
- package/dist/components/base/CkToolApproval/ArgumentsJson.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/BodyBoundary.vue.d.ts +15 -0
- package/dist/components/base/CkToolApproval/BodyBoundary.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/CardHeading.vue.d.ts +22 -0
- package/dist/components/base/CkToolApproval/CardHeading.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/CkToolApproval.stories.d.ts +80 -0
- package/dist/components/base/CkToolApproval/CkToolApproval.stories.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/CkToolApproval.vue.d.ts +18 -0
- package/dist/components/base/CkToolApproval/CkToolApproval.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/ResultRow.vue.d.ts +20 -0
- package/dist/components/base/CkToolApproval/ResultRow.vue.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/badge.d.ts +3 -0
- package/dist/components/base/CkToolApproval/badge.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/index.d.ts +3 -0
- package/dist/components/base/CkToolApproval/index.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/outcomes.d.ts +28 -0
- package/dist/components/base/CkToolApproval/outcomes.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/serializeArguments.d.ts +13 -0
- package/dist/components/base/CkToolApproval/serializeArguments.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/toolTitle.d.ts +7 -0
- package/dist/components/base/CkToolApproval/toolTitle.d.ts.map +1 -0
- package/dist/components/base/CkToolApproval/types.d.ts +102 -0
- package/dist/components/base/CkToolApproval/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -3
- package/dist/{createCrepeEditor-BGSlwmKn.js → createCrepeEditor-DSfByw4o.js} +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +39 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/{useChatKitLabels-DxW0N5U-.js → useChatKitLabels-ky3JI9gf.js} +13 -0
- package/package.json +3 -3
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +27 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +23 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +15 -0
- package/src/components/base/CkToolApproval/ApprovalCard.vue +30 -0
- package/src/components/base/CkToolApproval/ArgumentsJson.vue +48 -0
- package/src/components/base/CkToolApproval/BodyBoundary.vue +26 -0
- package/src/components/base/CkToolApproval/CardHeading.vue +47 -0
- package/src/components/base/CkToolApproval/CkToolApproval.spec.ts +680 -0
- package/src/components/base/CkToolApproval/CkToolApproval.stories.ts +661 -0
- package/src/components/base/CkToolApproval/CkToolApproval.vue +240 -0
- package/src/components/base/CkToolApproval/ResultRow.vue +52 -0
- package/src/components/base/CkToolApproval/badge.ts +2 -0
- package/src/components/base/CkToolApproval/exports.spec.ts +22 -0
- package/src/components/base/CkToolApproval/index.ts +2 -0
- package/src/components/base/CkToolApproval/outcomes.ts +95 -0
- package/src/components/base/CkToolApproval/serializeArguments.ts +18 -0
- package/src/components/base/CkToolApproval/toolTitle.spec.ts +23 -0
- package/src/components/base/CkToolApproval/toolTitle.ts +19 -0
- package/src/components/base/CkToolApproval/types.ts +118 -0
- package/src/components/base/index.ts +11 -0
- package/src/components/index.ts +9 -0
- package/src/handlers/useChatKitLabels.ts +39 -0
- package/src/index.ts +9 -0
- package/src/test-empty.css +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
ref,
|
|
6
|
+
toRaw,
|
|
7
|
+
useId,
|
|
8
|
+
useTemplateRef,
|
|
9
|
+
watch,
|
|
10
|
+
} from 'vue';
|
|
11
|
+
import { UiButton, UiSeparator } from '@aleph-alpha/ui-library';
|
|
12
|
+
import {
|
|
13
|
+
defaultChatKitLabels,
|
|
14
|
+
useChatKitLabels,
|
|
15
|
+
type ChatKitLabels,
|
|
16
|
+
} from '../../../handlers/useChatKitLabels';
|
|
17
|
+
import ApprovalCard from './ApprovalCard.vue';
|
|
18
|
+
import ArgumentsJson from './ArgumentsJson.vue';
|
|
19
|
+
import BodyBoundary from './BodyBoundary.vue';
|
|
20
|
+
import CardHeading from './CardHeading.vue';
|
|
21
|
+
import ResultRow from './ResultRow.vue';
|
|
22
|
+
import {
|
|
23
|
+
isDecidedStatus,
|
|
24
|
+
resultLineOf,
|
|
25
|
+
toolApprovalOutcome,
|
|
26
|
+
type CkToolApprovalOutcome,
|
|
27
|
+
} from './outcomes';
|
|
28
|
+
import { serializeArguments } from './serializeArguments';
|
|
29
|
+
import { toolTitleFromName } from './toolTitle';
|
|
30
|
+
import type {
|
|
31
|
+
CkToolApprovalEmits,
|
|
32
|
+
CkToolApprovalExposed,
|
|
33
|
+
CkToolApprovalProps,
|
|
34
|
+
CkToolApprovalSlots,
|
|
35
|
+
} from './types';
|
|
36
|
+
|
|
37
|
+
defineOptions({
|
|
38
|
+
name: 'CkToolApproval',
|
|
39
|
+
// The root is a fragment (card or row, plus the announcer); attrs go to the card or row.
|
|
40
|
+
inheritAttrs: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CONNECTOR_ICON = 'app-window';
|
|
44
|
+
|
|
45
|
+
const props = defineProps<CkToolApprovalProps>();
|
|
46
|
+
const emit = defineEmits<CkToolApprovalEmits>();
|
|
47
|
+
defineSlots<CkToolApprovalSlots>();
|
|
48
|
+
|
|
49
|
+
// useChatKitLabels types every label as optional; fill the gaps once so the
|
|
50
|
+
// buttons and the result row read the same, complete set.
|
|
51
|
+
const providedLabels = useChatKitLabels();
|
|
52
|
+
const labels = computed(() => ({
|
|
53
|
+
...defaultChatKitLabels,
|
|
54
|
+
...definedOnly(providedLabels.value),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// A label set to undefined falls back to the default instead of blanking the text.
|
|
58
|
+
function definedOnly(provided: ChatKitLabels): Partial<ChatKitLabels> {
|
|
59
|
+
return Object.fromEntries(
|
|
60
|
+
Object.entries(provided).filter(([, text]) => text !== undefined),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headingId = useId();
|
|
65
|
+
const pendingCard =
|
|
66
|
+
useTemplateRef<InstanceType<typeof ApprovalCard>>('pendingCard');
|
|
67
|
+
const resultRow = useTemplateRef<InstanceType<typeof ResultRow>>('resultRow');
|
|
68
|
+
|
|
69
|
+
// Locks both buttons after the first click, so a double click cannot send two
|
|
70
|
+
// answers. Released by a new request, any status change, or the host calling
|
|
71
|
+
// unlock() after a failed send.
|
|
72
|
+
const locked = ref(false);
|
|
73
|
+
|
|
74
|
+
function unlock(): void {
|
|
75
|
+
locked.value = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Separate sources: a fresh request object with the same id must not unlock.
|
|
79
|
+
watch([() => props.request.id, () => props.status], unlock);
|
|
80
|
+
|
|
81
|
+
function allow(): void {
|
|
82
|
+
lock();
|
|
83
|
+
emit('allow', props.request.id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deny(): void {
|
|
87
|
+
lock();
|
|
88
|
+
emit('deny', props.request.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function lock(): void {
|
|
92
|
+
// Browsers drop focus from a button once it is disabled; park it on the card
|
|
93
|
+
// so it can move on to the result row when the status arrives.
|
|
94
|
+
if (pendingCard.value?.containsFocus()) pendingCard.value.focus();
|
|
95
|
+
locked.value = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read aloud when a decision lands while focus is elsewhere in the page.
|
|
99
|
+
const announcement = ref('');
|
|
100
|
+
|
|
101
|
+
watch(
|
|
102
|
+
() => props.request.id,
|
|
103
|
+
() => {
|
|
104
|
+
announcement.value = '';
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// The clicked button disappears when the card collapses; move focus to the
|
|
109
|
+
// result so keyboard and screen-reader users land on the outcome. Without
|
|
110
|
+
// focus in the card, announce the outcome instead of taking focus.
|
|
111
|
+
watch(
|
|
112
|
+
() => props.status,
|
|
113
|
+
(status, previous) => {
|
|
114
|
+
if (previous !== 'pending' || status === 'pending') return;
|
|
115
|
+
if (pendingCard.value?.containsFocus()) {
|
|
116
|
+
void nextTick(() => resultRow.value?.focus());
|
|
117
|
+
} else {
|
|
118
|
+
announcement.value = resultLineOf(outcome.value);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
defineExpose<CkToolApprovalExposed>({ unlock });
|
|
124
|
+
|
|
125
|
+
const title = computed(
|
|
126
|
+
() => props.request.toolTitle ?? toolTitleFromName(props.request.toolName),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const connectorIcon = computed(
|
|
130
|
+
() => props.request.connectorIcon ?? DEFAULT_CONNECTOR_ICON,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// toRaw: a component map passed in reactive state would otherwise be rendered
|
|
134
|
+
// as a reactive object, which Vue warns about.
|
|
135
|
+
const customBody = computed(() => {
|
|
136
|
+
const toolBodies = ownEntry(props.bodyComponents, props.request.connector);
|
|
137
|
+
const body = ownEntry(toolBodies, props.request.toolName);
|
|
138
|
+
return body && toRaw(body);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// hasOwn: a tool named `constructor` must not resolve to Object.prototype's.
|
|
142
|
+
function ownEntry<T>(
|
|
143
|
+
record: Readonly<Record<string, T>> | undefined,
|
|
144
|
+
key: string,
|
|
145
|
+
): T | undefined {
|
|
146
|
+
return record && Object.hasOwn(record, key) ? record[key] : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const serializedArguments = computed(() =>
|
|
150
|
+
serializeArguments(props.request.arguments),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const outcome = computed((): CkToolApprovalOutcome => {
|
|
154
|
+
if (isDecidedStatus(props.status)) {
|
|
155
|
+
return toolApprovalOutcome(props.status, labels.value, props.summary);
|
|
156
|
+
}
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.warn(
|
|
159
|
+
`[CkToolApproval] Unknown status "${props.status}", showing it as failed.`,
|
|
160
|
+
);
|
|
161
|
+
return toolApprovalOutcome('failed', labels.value, props.summary);
|
|
162
|
+
});
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<template>
|
|
166
|
+
<ApprovalCard
|
|
167
|
+
v-if="status === 'pending'"
|
|
168
|
+
v-bind="$attrs"
|
|
169
|
+
ref="pendingCard"
|
|
170
|
+
:heading-id="headingId"
|
|
171
|
+
class="flex flex-col gap-3 py-4"
|
|
172
|
+
>
|
|
173
|
+
<CardHeading
|
|
174
|
+
:id="headingId"
|
|
175
|
+
class="px-6"
|
|
176
|
+
size="card"
|
|
177
|
+
:title="title"
|
|
178
|
+
:connector-label="request.connectorLabel"
|
|
179
|
+
:connector-icon="connectorIcon"
|
|
180
|
+
>
|
|
181
|
+
<template v-if="$slots['connector-icon']" #icon>
|
|
182
|
+
<slot name="connector-icon" />
|
|
183
|
+
</template>
|
|
184
|
+
</CardHeading>
|
|
185
|
+
|
|
186
|
+
<UiSeparator />
|
|
187
|
+
|
|
188
|
+
<div class="px-6 py-3">
|
|
189
|
+
<BodyBoundary v-if="customBody" :key="request.id">
|
|
190
|
+
<component :is="customBody" :request="request" />
|
|
191
|
+
<template #fallback>
|
|
192
|
+
<ArgumentsJson :serialized="serializedArguments" :labels="labels" />
|
|
193
|
+
</template>
|
|
194
|
+
</BodyBoundary>
|
|
195
|
+
<ArgumentsJson
|
|
196
|
+
v-else
|
|
197
|
+
:serialized="serializedArguments"
|
|
198
|
+
:labels="labels"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<UiSeparator />
|
|
203
|
+
|
|
204
|
+
<div class="flex items-center justify-end gap-2 px-6">
|
|
205
|
+
<UiButton variant="secondary" :disabled="locked" @click="deny">
|
|
206
|
+
{{ labels.toolApprovalDeny }}
|
|
207
|
+
</UiButton>
|
|
208
|
+
<!-- Nobody should allow arguments they cannot see. -->
|
|
209
|
+
<UiButton
|
|
210
|
+
:disabled="locked || !serializedArguments.readable"
|
|
211
|
+
@click="allow"
|
|
212
|
+
>
|
|
213
|
+
{{ labels.toolApprovalAllow }}
|
|
214
|
+
</UiButton>
|
|
215
|
+
</div>
|
|
216
|
+
</ApprovalCard>
|
|
217
|
+
|
|
218
|
+
<ResultRow
|
|
219
|
+
v-else
|
|
220
|
+
v-bind="$attrs"
|
|
221
|
+
ref="resultRow"
|
|
222
|
+
:outcome="outcome"
|
|
223
|
+
:heading-id="headingId"
|
|
224
|
+
>
|
|
225
|
+
<template #heading>
|
|
226
|
+
<CardHeading
|
|
227
|
+
:id="headingId"
|
|
228
|
+
size="row"
|
|
229
|
+
:title="title"
|
|
230
|
+
:connector-label="request.connectorLabel"
|
|
231
|
+
:connector-icon="connectorIcon"
|
|
232
|
+
>
|
|
233
|
+
<template v-if="$slots['connector-icon']" #icon>
|
|
234
|
+
<slot name="connector-icon" />
|
|
235
|
+
</template>
|
|
236
|
+
</CardHeading>
|
|
237
|
+
</template>
|
|
238
|
+
</ResultRow>
|
|
239
|
+
<span class="sr-only" aria-live="polite">{{ announcement }}</span>
|
|
240
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useId, useTemplateRef } from 'vue';
|
|
3
|
+
import { UiBadge, UiIcon } from '@aleph-alpha/ui-library';
|
|
4
|
+
import ApprovalCard from './ApprovalCard.vue';
|
|
5
|
+
import { BADGE_ICON_SIZE } from './badge';
|
|
6
|
+
import { resultLineOf, type CkToolApprovalOutcome } from './outcomes';
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
outcome: CkToolApprovalOutcome;
|
|
10
|
+
headingId: string;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
defineSlots<{
|
|
14
|
+
heading: () => unknown;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const descriptionId = useId();
|
|
18
|
+
const card = useTemplateRef<InstanceType<typeof ApprovalCard>>('card');
|
|
19
|
+
|
|
20
|
+
const resultLine = computed(() => resultLineOf(props.outcome));
|
|
21
|
+
|
|
22
|
+
defineExpose({
|
|
23
|
+
focus: (): void => card.value?.focus(),
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<ApprovalCard
|
|
29
|
+
ref="card"
|
|
30
|
+
:heading-id="headingId"
|
|
31
|
+
:description-id="descriptionId"
|
|
32
|
+
class="flex items-center gap-3 px-5 py-4"
|
|
33
|
+
>
|
|
34
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
35
|
+
<slot name="heading" />
|
|
36
|
+
<p
|
|
37
|
+
:id="descriptionId"
|
|
38
|
+
class="truncate text-xs leading-[14px] text-content-on-surface-muted"
|
|
39
|
+
>
|
|
40
|
+
{{ resultLine }}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
<UiBadge :variant="outcome.badge.variant" size="small" class="shrink-0">
|
|
44
|
+
<UiIcon
|
|
45
|
+
:name="outcome.badge.icon"
|
|
46
|
+
:size="BADGE_ICON_SIZE"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
/>
|
|
49
|
+
{{ outcome.badge.text }}
|
|
50
|
+
</UiBadge>
|
|
51
|
+
</ApprovalCard>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import CkToolApproval from './CkToolApproval.vue';
|
|
2
|
+
import * as components from '../../index';
|
|
3
|
+
import * as packageRoot from '../../../index';
|
|
4
|
+
import type {
|
|
5
|
+
CkToolApprovalBodyProps,
|
|
6
|
+
CkToolApprovalExposed,
|
|
7
|
+
} from '../../../index';
|
|
8
|
+
|
|
9
|
+
describe('CkToolApproval exports', () => {
|
|
10
|
+
it('is part of the public components entry', () => {
|
|
11
|
+
expect(components.CkToolApproval).toBe(CkToolApproval);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('is part of the package entry consumers import from', () => {
|
|
15
|
+
expect(packageRoot.CkToolApproval).toBe(CkToolApproval);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('exports the types a host needs for a custom body and for unlock()', () => {
|
|
19
|
+
expectTypeOf<CkToolApprovalBodyProps>().toHaveProperty('request');
|
|
20
|
+
expectTypeOf<CkToolApprovalExposed>().toHaveProperty('unlock');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { UiBadgeVariants } from '@aleph-alpha/ui-library';
|
|
2
|
+
import type { ChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
3
|
+
import { ckToolApprovalStatuses, type CkToolApprovalStatus } from './types';
|
|
4
|
+
|
|
5
|
+
/** A status the user has already decided, shown as the collapsed result row. */
|
|
6
|
+
export type CkToolApprovalDecidedStatus = Exclude<
|
|
7
|
+
CkToolApprovalStatus,
|
|
8
|
+
'pending'
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
/** Guards against statuses a JavaScript host may pass that the type does not allow. */
|
|
12
|
+
export function isDecidedStatus(
|
|
13
|
+
status: string,
|
|
14
|
+
): status is CkToolApprovalDecidedStatus {
|
|
15
|
+
return (
|
|
16
|
+
status !== 'pending' &&
|
|
17
|
+
ckToolApprovalStatuses.some((known) => known === status)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The status pill on the right of the result row. */
|
|
22
|
+
export interface CkToolApprovalBadge {
|
|
23
|
+
variant: Extract<UiBadgeVariants, 'verified' | 'secondary' | 'destructive'>;
|
|
24
|
+
icon: string;
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CkToolApprovalOutcome {
|
|
29
|
+
/** The outcome half of the result line, e.g. "Denied by you". */
|
|
30
|
+
result: string;
|
|
31
|
+
/** Detail after the outcome, e.g. "Nothing was updated". Empty to omit. */
|
|
32
|
+
detail: string;
|
|
33
|
+
badge: CkToolApprovalBadge;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const RESULT_SEPARATOR = ' · ';
|
|
37
|
+
|
|
38
|
+
/** The one-line outcome, e.g. "Denied by you · Nothing was updated". */
|
|
39
|
+
export function resultLineOf(outcome: CkToolApprovalOutcome): string {
|
|
40
|
+
return [outcome.result, outcome.detail]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(RESULT_SEPARATOR);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The result line and badge for each decided status, from the design's
|
|
47
|
+
* "HITL Request" component and its Skipped variant.
|
|
48
|
+
*/
|
|
49
|
+
export function toolApprovalOutcome(
|
|
50
|
+
status: CkToolApprovalDecidedStatus,
|
|
51
|
+
labels: Required<ChatKitLabels>,
|
|
52
|
+
summary?: string,
|
|
53
|
+
): CkToolApprovalOutcome {
|
|
54
|
+
// Every status where the call never ran shares the same fallback detail.
|
|
55
|
+
const notRunDetail = summary ?? labels.toolApprovalNothingChanged;
|
|
56
|
+
const outcomes: Record<CkToolApprovalDecidedStatus, CkToolApprovalOutcome> = {
|
|
57
|
+
allowed: {
|
|
58
|
+
result: labels.toolApprovalAllowedResult,
|
|
59
|
+
detail: summary ?? '',
|
|
60
|
+
badge: {
|
|
61
|
+
variant: 'verified',
|
|
62
|
+
icon: 'check',
|
|
63
|
+
text: labels.toolApprovalAllowedBadge,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
denied: {
|
|
67
|
+
result: labels.toolApprovalDeniedResult,
|
|
68
|
+
detail: notRunDetail,
|
|
69
|
+
badge: {
|
|
70
|
+
variant: 'secondary',
|
|
71
|
+
icon: 'ban',
|
|
72
|
+
text: labels.toolApprovalDeniedBadge,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
failed: {
|
|
76
|
+
result: labels.toolApprovalFailedResult,
|
|
77
|
+
detail: notRunDetail,
|
|
78
|
+
badge: {
|
|
79
|
+
variant: 'destructive',
|
|
80
|
+
icon: 'triangle-alert',
|
|
81
|
+
text: labels.toolApprovalFailedBadge,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
skipped: {
|
|
85
|
+
result: labels.toolApprovalSkippedResult,
|
|
86
|
+
detail: notRunDetail,
|
|
87
|
+
badge: {
|
|
88
|
+
variant: 'secondary',
|
|
89
|
+
icon: 'skip-forward',
|
|
90
|
+
text: labels.toolApprovalSkippedBadge,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
return outcomes[status];
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The arguments as JSON text, or the fact that they cannot be shown. */
|
|
2
|
+
export type SerializedArguments =
|
|
3
|
+
| { readable: true; json: string }
|
|
4
|
+
| { readable: false };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turns the arguments into indented JSON. Values JSON cannot hold, such as a
|
|
8
|
+
* bigint or a circular reference, make them unreadable instead of throwing.
|
|
9
|
+
*/
|
|
10
|
+
export function serializeArguments(args: unknown): SerializedArguments {
|
|
11
|
+
try {
|
|
12
|
+
return { readable: true, json: JSON.stringify(args, null, 2) };
|
|
13
|
+
} catch (error) {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.error('[CkToolApproval] Arguments cannot be shown as JSON:', error);
|
|
16
|
+
return { readable: false };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { toolTitleFromName } from './toolTitle';
|
|
2
|
+
|
|
3
|
+
describe('toolTitleFromName', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['update_work_items', 'Update work items'],
|
|
6
|
+
['createJiraIssue', 'Create jira issue'],
|
|
7
|
+
['send-message', 'Send message'],
|
|
8
|
+
['GetDocument', 'Get document'],
|
|
9
|
+
['list__projects', 'List projects'],
|
|
10
|
+
['search', 'Search'],
|
|
11
|
+
['upload_file_v2', 'Upload file v2'],
|
|
12
|
+
['getHTTPResponse', 'Get HTTP response'],
|
|
13
|
+
['getPDF', 'Get PDF'],
|
|
14
|
+
['listMCPServers', 'List MCP servers'],
|
|
15
|
+
['admin.tools.list', 'Admin tools list'],
|
|
16
|
+
])('turns %s into %s', (name, title) => {
|
|
17
|
+
expect(toolTitleFromName(name)).toBe(title);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps an empty name empty instead of inventing a title', () => {
|
|
21
|
+
expect(toolTitleFromName('')).toBe('');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a machine tool name into a readable action, for tools whose server
|
|
3
|
+
* gives no human title: `update_work_items` and `getPDF` become
|
|
4
|
+
* "Update work items" and "Get PDF".
|
|
5
|
+
*/
|
|
6
|
+
export function toolTitleFromName(toolName: string): string {
|
|
7
|
+
const words = toolName
|
|
8
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
9
|
+
.replace(/([A-Z])(?=[A-Z][a-z])/g, '$1 ')
|
|
10
|
+
.split(/[\s_.-]+/)
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((word) => (isAcronym(word) ? word : word.toLowerCase()));
|
|
13
|
+
const sentence = words.join(' ');
|
|
14
|
+
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isAcronym(word: string): boolean {
|
|
18
|
+
return /^[A-Z]{2,}$/.test(word);
|
|
19
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where an approval stands: waiting for the user, or decided.
|
|
5
|
+
* `failed` means the user allowed the call but the tool errored.
|
|
6
|
+
* `skipped` means the user sent a new message instead of answering.
|
|
7
|
+
*/
|
|
8
|
+
export const ckToolApprovalStatuses = [
|
|
9
|
+
'pending',
|
|
10
|
+
'allowed',
|
|
11
|
+
'denied',
|
|
12
|
+
'failed',
|
|
13
|
+
'skipped',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type CkToolApprovalStatus = (typeof ckToolApprovalStatuses)[number];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A tool call the agent wants to run and is waiting on the user for.
|
|
20
|
+
* Transport-neutral: the host converts its own wire format (a Responses API
|
|
21
|
+
* `mcp_approval_request`, an agent interrupt, ...) into this shape.
|
|
22
|
+
*/
|
|
23
|
+
export interface CkToolApprovalRequest {
|
|
24
|
+
/** Id the host needs to answer the request, emitted back with the decision. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Machine name of the connector the tool belongs to, e.g. `polarion`. */
|
|
27
|
+
connector: string;
|
|
28
|
+
/**
|
|
29
|
+
* Display name of the connector, shown as a chip next to the header, e.g.
|
|
30
|
+
* `"Polarion"`. Omit to hide the chip, for example when the chat is
|
|
31
|
+
* embedded in the connector's own app.
|
|
32
|
+
*/
|
|
33
|
+
connectorLabel?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Lucide icon shown in the connector chip, e.g. `"mail"`. Defaults to
|
|
36
|
+
* `"app-window"`. For a brand logo, use the `connector-icon` slot instead.
|
|
37
|
+
*/
|
|
38
|
+
connectorIcon?: string;
|
|
39
|
+
/** Machine name of the tool, e.g. `update_work_items`. */
|
|
40
|
+
toolName: string;
|
|
41
|
+
/**
|
|
42
|
+
* Human title of the tool, shown as the header, e.g. `"Update work items"`.
|
|
43
|
+
* MCP servers provide one per tool. When omitted, the header is derived
|
|
44
|
+
* from `toolName`.
|
|
45
|
+
*/
|
|
46
|
+
toolTitle?: string;
|
|
47
|
+
/** The arguments the agent wants to call the tool with, already parsed. */
|
|
48
|
+
arguments: Readonly<Record<string, unknown>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Custom approval bodies, keyed by connector, then by tool name. A request
|
|
53
|
+
* with no entry falls back to the JSON view. Each component receives the
|
|
54
|
+
* request as its `request` prop.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* { polarion: { update_work_items: PolarionWorkItemsBody } }
|
|
58
|
+
*/
|
|
59
|
+
export type CkToolApprovalBodyComponents = Readonly<
|
|
60
|
+
Record<string, Readonly<Record<string, Component<CkToolApprovalBodyProps>>>>
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
/** Props every custom approval body receives. */
|
|
64
|
+
export interface CkToolApprovalBodyProps {
|
|
65
|
+
request: CkToolApprovalRequest;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Asks the user to allow or deny a tool call the agent paused on, then
|
|
70
|
+
* collapses to a one-line result once decided. Shows the call's arguments as
|
|
71
|
+
* JSON unless the host registers a custom body for that connector and tool.
|
|
72
|
+
*
|
|
73
|
+
* Purely presentational: the host owns the request, sends the decision to its
|
|
74
|
+
* backend and passes the resulting `status` back in. Emits `allow` or `deny`
|
|
75
|
+
* with the request id, then locks both buttons until `status` changes or the
|
|
76
|
+
* host calls the exposed `unlock()`.
|
|
77
|
+
*
|
|
78
|
+
* @category Feedback
|
|
79
|
+
* @useCases human in the loop approval, confirm a tool call before it runs, show the outcome of a tool call
|
|
80
|
+
* @keywords approval, hitl, human in the loop, tool call, confirm, allow, deny, permission, mcp
|
|
81
|
+
* @related CkAgentMessage, CkConversation
|
|
82
|
+
*/
|
|
83
|
+
export interface CkToolApprovalProps {
|
|
84
|
+
/** The tool call waiting for a decision. */
|
|
85
|
+
request: CkToolApprovalRequest;
|
|
86
|
+
/** Current state. `pending` shows the full card with Deny and Allow. */
|
|
87
|
+
status: CkToolApprovalStatus;
|
|
88
|
+
/**
|
|
89
|
+
* Outcome detail after the result, for any decided status, e.g.
|
|
90
|
+
* `"2 work items updated"` or `"Nothing was sent"`. Pass a fully formatted,
|
|
91
|
+
* localized string. When omitted, allowed shows no detail and the other
|
|
92
|
+
* statuses fall back to `toolApprovalNothingChanged` ("Nothing was updated").
|
|
93
|
+
* Pass an empty string to show no detail.
|
|
94
|
+
*/
|
|
95
|
+
summary?: string;
|
|
96
|
+
/** Custom bodies per connector and tool, replacing the JSON view. */
|
|
97
|
+
bodyComponents?: CkToolApprovalBodyComponents;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CkToolApprovalSlots {
|
|
101
|
+
/** Replaces the connector chip's icon, e.g. with a brand logo image. */
|
|
102
|
+
'connector-icon'?: () => unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CkToolApprovalEmits {
|
|
106
|
+
/** The user allowed the call. Payload is `request.id`. */
|
|
107
|
+
(e: 'allow', id: string): void;
|
|
108
|
+
/** The user denied the call. Payload is `request.id`. */
|
|
109
|
+
(e: 'deny', id: string): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CkToolApprovalExposed {
|
|
113
|
+
/**
|
|
114
|
+
* Re-enables Allow and Deny after a click. Call it when sending the
|
|
115
|
+
* decision failed and the request stays pending, so the user can retry.
|
|
116
|
+
*/
|
|
117
|
+
unlock: () => void;
|
|
118
|
+
}
|
|
@@ -73,6 +73,17 @@ export { CkPromptStopButton } from './CkPromptStopButton';
|
|
|
73
73
|
export { CkPromptInput } from './CkPromptInput';
|
|
74
74
|
export { CkPromptInputDisclaimer } from './CkPromptInputDisclaimer';
|
|
75
75
|
export { CkRegenerateMessageButton } from './CkRegenerateMessageButton';
|
|
76
|
+
export { CkToolApproval } from './CkToolApproval';
|
|
77
|
+
export type {
|
|
78
|
+
CkToolApprovalProps,
|
|
79
|
+
CkToolApprovalEmits,
|
|
80
|
+
CkToolApprovalSlots,
|
|
81
|
+
CkToolApprovalRequest,
|
|
82
|
+
CkToolApprovalStatus,
|
|
83
|
+
CkToolApprovalBodyComponents,
|
|
84
|
+
CkToolApprovalBodyProps,
|
|
85
|
+
CkToolApprovalExposed,
|
|
86
|
+
} from './CkToolApproval';
|
|
76
87
|
export { CkUserMessage } from './CkUserMessage';
|
|
77
88
|
export type { CkUserMessageProps } from './CkUserMessage';
|
|
78
89
|
export { CkEditableUserMessage } from './CkEditableUserMessage';
|
package/src/components/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
CkRegenerateMessageButton,
|
|
26
26
|
CkSendIcon,
|
|
27
27
|
CkStopIcon,
|
|
28
|
+
CkToolApproval,
|
|
28
29
|
CkUserMessage,
|
|
29
30
|
CkEditableUserMessage,
|
|
30
31
|
CkMilkdownEditor,
|
|
@@ -66,6 +67,14 @@ export type {
|
|
|
66
67
|
CkModelSelectorProps,
|
|
67
68
|
CkModelSelectorEmits,
|
|
68
69
|
CkPromptMenuButtonProps,
|
|
70
|
+
CkToolApprovalProps,
|
|
71
|
+
CkToolApprovalEmits,
|
|
72
|
+
CkToolApprovalSlots,
|
|
73
|
+
CkToolApprovalRequest,
|
|
74
|
+
CkToolApprovalStatus,
|
|
75
|
+
CkToolApprovalBodyComponents,
|
|
76
|
+
CkToolApprovalBodyProps,
|
|
77
|
+
CkToolApprovalExposed,
|
|
69
78
|
CkUserMessageProps,
|
|
70
79
|
CkEditableUserMessageProps,
|
|
71
80
|
CkEditableUserMessageEmits,
|