@aleph-alpha/chat-kit 3.3.0 → 4.0.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/{CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js → CkInput.vue_vue_type_script_setup_true_lang-DJN-nZ2-.js} +118 -71
- package/dist/adapters/responses-api.d.ts +11 -5
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -3
- package/dist/chat-kit.css +8 -8
- package/dist/components/base/CkAgentMessage/types.d.ts +1 -1
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
- package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
- package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
- package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +3 -2
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
- package/dist/components/composed/CkConversation/types.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +7 -6
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/useChatKitLabels.d.ts +2 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMessageHandler.d.ts +37 -0
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +15 -0
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/{index-C6njJ8EB.js → index-CyrVnbS9.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/{useChatKitLabels-BM2I7-9o.js → useChatKitLabels-zMFV7aEz.js} +1 -0
- package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
- package/package.json +3 -3
- package/src/adapters/responses-api.spec.ts +52 -5
- package/src/adapters/responses-api.ts +26 -7
- package/src/components/base/CkAgentMessage/types.ts +1 -1
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
- package/src/components/base/CkCopyMessageButton/index.ts +2 -0
- package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
- package/src/components/base/CkFeedbackButton/types.ts +1 -1
- package/src/components/base/CkFeedbackDialog/types.ts +1 -1
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
- package/src/components/base/CkMessageButton/index.ts +2 -0
- package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
- package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
- package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
- package/src/components/base/index.ts +3 -2
- package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
- package/src/components/composed/CkConversation/CkConversation.vue +2 -2
- package/src/components/composed/CkConversation/types.ts +1 -1
- package/src/components/index.ts +3 -2
- package/src/handlers/useChatKitLabels.spec.ts +1 -0
- package/src/handlers/useChatKitLabels.ts +3 -0
- package/src/handlers/useMessageHandler.spec.ts +79 -0
- package/src/handlers/useMessageHandler.ts +50 -2
- package/src/helpers/conversationTree.spec.ts +120 -0
- package/src/helpers/conversationTree.ts +57 -8
- package/src/index.ts +3 -2
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
- package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
- package/src/components/base/CkMessageActionButton/index.ts +0 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import CkRegenerateMessageButton from './CkRegenerateMessageButton.vue';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/vue';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('CkRegenerateMessageButton', () => {
|
|
7
|
+
it('renders an accessible button with the default label', () => {
|
|
8
|
+
render(CkRegenerateMessageButton);
|
|
9
|
+
|
|
10
|
+
expect(
|
|
11
|
+
screen.getByRole('button', { name: 'Regenerate response' }),
|
|
12
|
+
).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('uses a custom aria-label when provided', () => {
|
|
16
|
+
render(CkRegenerateMessageButton, {
|
|
17
|
+
props: { ariaLabel: 'Try again' },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
screen.getByRole('button', { name: 'Try again' }),
|
|
22
|
+
).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('emits regenerate when clicked', async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
const { emitted } = render(CkRegenerateMessageButton);
|
|
28
|
+
|
|
29
|
+
await user.click(
|
|
30
|
+
screen.getByRole('button', { name: 'Regenerate response' }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(emitted('regenerate')).toEqual([[]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not emit regenerate when disabled', async () => {
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
const { emitted } = render(CkRegenerateMessageButton, {
|
|
39
|
+
props: { disabled: true },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const button = screen.getByRole('button', { name: 'Regenerate response' });
|
|
43
|
+
expect(button).toBeDisabled();
|
|
44
|
+
|
|
45
|
+
await user.click(button);
|
|
46
|
+
|
|
47
|
+
expect(emitted('regenerate')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import CkRegenerateMessageButton from './CkRegenerateMessageButton.vue';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof CkRegenerateMessageButton> = {
|
|
5
|
+
title: 'Base/CkRegenerateMessageButton',
|
|
6
|
+
component: CkRegenerateMessageButton,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
disabled: {
|
|
10
|
+
control: 'boolean',
|
|
11
|
+
description: 'Whether the button is disabled and ignores clicks.',
|
|
12
|
+
},
|
|
13
|
+
ariaLabel: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'Accessible label and tooltip content for the button.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
decorators: [
|
|
19
|
+
() => ({
|
|
20
|
+
template:
|
|
21
|
+
'<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
30
|
+
import { CkRegenerateMessageButton } from '@aleph-alpha/chat-kit';
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<CkRegenerateMessageButton @regenerate="() => {}" />
|
|
35
|
+
</template>`;
|
|
36
|
+
|
|
37
|
+
const disabledTemplateSource = `<script setup lang="ts">
|
|
38
|
+
import { CkRegenerateMessageButton } from '@aleph-alpha/chat-kit';
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<CkRegenerateMessageButton disabled />
|
|
43
|
+
</template>`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default regenerate action. Clicking emits a `regenerate` event so the host
|
|
47
|
+
* can request a new response for the message.
|
|
48
|
+
*/
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
args: {},
|
|
51
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Disabled state — clicks are ignored and the button appears inactive.
|
|
56
|
+
* Use while a message is still streaming or in an error state.
|
|
57
|
+
*/
|
|
58
|
+
export const Disabled: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
disabled: true,
|
|
61
|
+
},
|
|
62
|
+
parameters: { docs: { source: { code: disabledTemplateSource } } },
|
|
63
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CkMessageButton } from '../CkMessageButton';
|
|
3
|
+
import type { CkRegenerateMessageButtonProps } from './types';
|
|
4
|
+
import { UiIcon, UiTooltip } from '@aleph-alpha/ui-library';
|
|
5
|
+
|
|
6
|
+
defineOptions({
|
|
7
|
+
name: 'CkRegenerateMessageButton',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<CkRegenerateMessageButtonProps>(), {
|
|
11
|
+
disabled: false,
|
|
12
|
+
ariaLabel: 'Regenerate response',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
regenerate: [];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
function handleClick() {
|
|
20
|
+
if (props.disabled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
emit('regenerate');
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<UiTooltip
|
|
30
|
+
:content="ariaLabel"
|
|
31
|
+
side="bottom"
|
|
32
|
+
align="center"
|
|
33
|
+
:delay-duration="0"
|
|
34
|
+
>
|
|
35
|
+
<CkMessageButton
|
|
36
|
+
v-bind="{ ariaLabel }"
|
|
37
|
+
:disabled="disabled"
|
|
38
|
+
@click="handleClick"
|
|
39
|
+
>
|
|
40
|
+
<UiIcon name="repeat" :size="16" aria-hidden="true" />
|
|
41
|
+
</CkMessageButton>
|
|
42
|
+
</UiTooltip>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate action button for chat messages. Lets the user request a new
|
|
3
|
+
* response for an assistant message. Renders an icon-only button with an
|
|
4
|
+
* accessible label and tooltip.
|
|
5
|
+
*
|
|
6
|
+
* @category Actions
|
|
7
|
+
* @useCases regenerate assistant reply, retry message, request new response, chat message action
|
|
8
|
+
* @keywords regenerate, retry, repeat, action, message, button, icon
|
|
9
|
+
* @related CkMessageButton, CkCopyMessageButton, CkAgentMessage, CkConversation
|
|
10
|
+
*/
|
|
11
|
+
export interface CkRegenerateMessageButtonProps {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the button is disabled and ignores clicks.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Accessible label and tooltip content for the button.
|
|
19
|
+
* @default 'Regenerate response'
|
|
20
|
+
*/
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CkRegenerateMessageButtonEmits {
|
|
25
|
+
/** Emitted when the button is pressed and not disabled. */
|
|
26
|
+
(e: 'regenerate'): void;
|
|
27
|
+
}
|
|
@@ -13,7 +13,7 @@ export type {
|
|
|
13
13
|
} from './CkCitationCard';
|
|
14
14
|
export { CkConversationLayout } from './CkConversationLayout';
|
|
15
15
|
export type { CkConversationLayoutProps } from './CkConversationLayout';
|
|
16
|
-
export {
|
|
16
|
+
export { CkCopyMessageButton } from './CkCopyMessageButton';
|
|
17
17
|
export { CkDropZoneOverlay } from './CkDropZoneOverlay';
|
|
18
18
|
export type {
|
|
19
19
|
CkDropZoneOverlayProps,
|
|
@@ -50,7 +50,7 @@ export type {
|
|
|
50
50
|
CkMarkdownRendererEmits,
|
|
51
51
|
CkMarkdownElementClasses,
|
|
52
52
|
} from './CkMarkdownRenderer';
|
|
53
|
-
export {
|
|
53
|
+
export { CkMessageButton } from './CkMessageButton';
|
|
54
54
|
export { CkModelSelector } from './CkModelSelector';
|
|
55
55
|
export type {
|
|
56
56
|
CkModelOption,
|
|
@@ -63,5 +63,6 @@ export type { CkPromptMenuButtonProps } from './CkPromptMenuButton';
|
|
|
63
63
|
export { CkPromptStopButton } from './CkPromptStopButton';
|
|
64
64
|
export { CkPromptInput } from './CkPromptInput';
|
|
65
65
|
export { CkPromptInputDisclaimer } from './CkPromptInputDisclaimer';
|
|
66
|
+
export { CkRegenerateMessageButton } from './CkRegenerateMessageButton';
|
|
66
67
|
export { CkUserMessage } from './CkUserMessage';
|
|
67
68
|
export type { CkUserMessageProps } from './CkUserMessage';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CkCopyMessageButton } from '../../base/CkCopyMessageButton';
|
|
2
2
|
import { CkFeedbackButton } from '../../base/CkFeedbackButton';
|
|
3
3
|
import type { CkFeedbackValue } from '../../base/CkFeedbackButton';
|
|
4
4
|
import type {
|
|
@@ -292,7 +292,7 @@ const withFooterActionsTemplateSource = `<script setup lang="ts">
|
|
|
292
292
|
import { ref } from 'vue';
|
|
293
293
|
import {
|
|
294
294
|
CkConversation,
|
|
295
|
-
|
|
295
|
+
CkCopyMessageButton,
|
|
296
296
|
CkFeedbackButton,
|
|
297
297
|
} from '@aleph-alpha/chat-kit';
|
|
298
298
|
import type { BaseMessage, CkFeedbackValue } from '@aleph-alpha/chat-kit';
|
|
@@ -315,7 +315,7 @@ function onFeedback(messageId: string, value: CkFeedbackValue) {
|
|
|
315
315
|
<template>
|
|
316
316
|
<CkConversation :messages="messages" agent-avatar-fallback="AI" style="height: 100%;">
|
|
317
317
|
<template #agent-message-footer="{ message }">
|
|
318
|
-
<
|
|
318
|
+
<CkCopyMessageButton :text="getMessageText(message)" />
|
|
319
319
|
<CkFeedbackButton
|
|
320
320
|
:value="feedback[message.id]?.value ?? null"
|
|
321
321
|
:locked="feedback[message.id]?.locked ?? false"
|
|
@@ -590,7 +590,7 @@ export const WithCustomIcon: Story = {
|
|
|
590
590
|
|
|
591
591
|
/**
|
|
592
592
|
* Overrides `#agent-message-footer` to add real action buttons next to each
|
|
593
|
-
* assistant reply: `
|
|
593
|
+
* assistant reply: `CkCopyMessageButton` for clipboard copy and
|
|
594
594
|
* `CkFeedbackButton` for binary thumbs up / thumbs down feedback. The
|
|
595
595
|
* feedback state is owned by the host and locked once submitted.
|
|
596
596
|
*/
|
|
@@ -598,7 +598,7 @@ export const WithFooterActions: Story = {
|
|
|
598
598
|
render: () => ({
|
|
599
599
|
components: {
|
|
600
600
|
CkConversation,
|
|
601
|
-
|
|
601
|
+
CkCopyMessageButton,
|
|
602
602
|
CkFeedbackButton,
|
|
603
603
|
},
|
|
604
604
|
setup() {
|
|
@@ -618,7 +618,7 @@ export const WithFooterActions: Story = {
|
|
|
618
618
|
template: `
|
|
619
619
|
<CkConversation :messages="messages" agent-avatar-fallback="AI" style="height: 100%;">
|
|
620
620
|
<template #agent-message-footer="{ message }">
|
|
621
|
-
<
|
|
621
|
+
<CkCopyMessageButton :text="getMessageText(message)" />
|
|
622
622
|
<CkFeedbackButton
|
|
623
623
|
:value="feedback[message.id]?.value ?? null"
|
|
624
624
|
:locked="feedback[message.id]?.locked ?? false"
|
|
@@ -3,7 +3,7 @@ import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
|
3
3
|
import type { BaseMessage, MessageTextContent } from '../../../types';
|
|
4
4
|
import CkAgentMessage from '../../base/CkAgentMessage/CkAgentMessage.vue';
|
|
5
5
|
import CkConversationLayout from '../../base/CkConversationLayout/CkConversationLayout.vue';
|
|
6
|
-
import
|
|
6
|
+
import CkCopyMessageButton from '../../base/CkCopyMessageButton/CkCopyMessageButton.vue';
|
|
7
7
|
import CkMarkdownRenderer from '../../base/CkMarkdownRenderer/CkMarkdownRenderer.vue';
|
|
8
8
|
import CkUserMessage from '../../base/CkUserMessage/CkUserMessage.vue';
|
|
9
9
|
import type { CkConversationProps } from './types';
|
|
@@ -143,7 +143,7 @@ function getMessageText(message: BaseMessage): string {
|
|
|
143
143
|
|
|
144
144
|
<template #footer>
|
|
145
145
|
<slot name="agent-message-footer" :message="message">
|
|
146
|
-
<
|
|
146
|
+
<CkCopyMessageButton
|
|
147
147
|
v-if="getMessageText(message) && !isLoading(message)"
|
|
148
148
|
:text="getMessageText(message)"
|
|
149
149
|
:aria-label-copy="labels.copyMessageAction"
|
|
@@ -10,7 +10,7 @@ import type { CkMarkdownElementClasses } from '../../base/CkMarkdownRenderer';
|
|
|
10
10
|
* @category Messages
|
|
11
11
|
* @useCases complete chat conversation ui, streaming chat with markdown, assistant chat view, llm conversation component, drop-in chat surface
|
|
12
12
|
* @keywords conversation, chat, messages, assistant, agent, user, markdown, streaming, avatar
|
|
13
|
-
* @related CkConversationLayout, CkAgentMessage, CkUserMessage, CkMarkdownRenderer,
|
|
13
|
+
* @related CkConversationLayout, CkAgentMessage, CkUserMessage, CkMarkdownRenderer, CkCopyMessageButton, CkInput
|
|
14
14
|
*/
|
|
15
15
|
export interface CkConversationProps {
|
|
16
16
|
/** List of messages to display, in chronological order. */
|
package/src/components/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
CkCitation,
|
|
5
5
|
CkCitationCard,
|
|
6
6
|
CkConversationLayout,
|
|
7
|
-
|
|
7
|
+
CkCopyMessageButton,
|
|
8
8
|
CkDropZoneOverlay,
|
|
9
9
|
CkFeedbackButton,
|
|
10
10
|
CkFeedbackDialog,
|
|
@@ -12,13 +12,14 @@ export {
|
|
|
12
12
|
CkHistoryItem,
|
|
13
13
|
CkHistoryWrapper,
|
|
14
14
|
CkMarkdownRenderer,
|
|
15
|
-
|
|
15
|
+
CkMessageButton,
|
|
16
16
|
CkModelSelector,
|
|
17
17
|
CkPromptSendButton,
|
|
18
18
|
CkPromptMenuButton,
|
|
19
19
|
CkPromptStopButton,
|
|
20
20
|
CkPromptInput,
|
|
21
21
|
CkPromptInputDisclaimer,
|
|
22
|
+
CkRegenerateMessageButton,
|
|
22
23
|
CkSendIcon,
|
|
23
24
|
CkStopIcon,
|
|
24
25
|
CkUserMessage,
|
|
@@ -102,6 +102,7 @@ describe('useChatKitLabels', () => {
|
|
|
102
102
|
inputPlaceholder: 'Custom placeholder',
|
|
103
103
|
copyMessageAction: 'Custom copy',
|
|
104
104
|
copyMessageActionCopied: 'Custom copied',
|
|
105
|
+
regenerateMessageAction: 'Custom regenerate',
|
|
105
106
|
feedbackThumbsUp: 'Custom up',
|
|
106
107
|
feedbackThumbsDown: 'Custom down',
|
|
107
108
|
feedbackGroup: 'Custom group',
|
|
@@ -29,6 +29,8 @@ export type ChatKitLabels = {
|
|
|
29
29
|
copyMessageAction: string;
|
|
30
30
|
/** Accessible label / tooltip for the copy-message action button after a successful copy */
|
|
31
31
|
copyMessageActionCopied: string;
|
|
32
|
+
/** Accessible label / tooltip for the regenerate-message action button */
|
|
33
|
+
regenerateMessageAction: string;
|
|
32
34
|
/**
|
|
33
35
|
* Accessible label / tooltip for the thumbs-up button in `CkFeedbackButton`.
|
|
34
36
|
* Optional for backward compatibility; falls back to
|
|
@@ -114,6 +116,7 @@ export const defaultChatKitLabels: ChatKitLabels = {
|
|
|
114
116
|
inputPlaceholder: 'Type a message...',
|
|
115
117
|
copyMessageAction: 'Copy response',
|
|
116
118
|
copyMessageActionCopied: 'Copied!',
|
|
119
|
+
regenerateMessageAction: 'Regenerate response',
|
|
117
120
|
feedbackThumbsUp: 'Good response',
|
|
118
121
|
feedbackThumbsDown: 'Bad response',
|
|
119
122
|
feedbackGroup: 'Rate this response',
|
|
@@ -146,6 +146,36 @@ describe('useMessageHandler', () => {
|
|
|
146
146
|
expect('metadata' in msg).toBe(false);
|
|
147
147
|
expect('metadata' in handler.messages.value[0]).toBe(false);
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('branches off an explicit parentId, updating the parent and activating the new branch', () => {
|
|
151
|
+
const handler = useMessageHandler();
|
|
152
|
+
|
|
153
|
+
handler.setCurrentUserMessage('First');
|
|
154
|
+
const first = handler.commitUserMessage();
|
|
155
|
+
handler.setCurrentUserMessage('Second');
|
|
156
|
+
const second = handler.commitUserMessage();
|
|
157
|
+
|
|
158
|
+
// Branch a sibling of `second` off their shared parent `first`.
|
|
159
|
+
handler.setCurrentUserMessage('Sibling');
|
|
160
|
+
const sibling = handler.commitUserMessage({ parentId: first.id });
|
|
161
|
+
|
|
162
|
+
const parent = handler.getTree().nodes[first.id];
|
|
163
|
+
expect(parent.childrenIds).toEqual([second.id, sibling.id]);
|
|
164
|
+
// The freshly created branch becomes the active path.
|
|
165
|
+
expect(parent.activeChildIndex).toBe(1);
|
|
166
|
+
expect(handler.messages.value.map((m) => m.id)).toEqual([
|
|
167
|
+
first.id,
|
|
168
|
+
sibling.id,
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('throws when branching off an unknown parentId', () => {
|
|
173
|
+
const handler = useMessageHandler();
|
|
174
|
+
handler.setCurrentUserMessage('Hello');
|
|
175
|
+
expect(() =>
|
|
176
|
+
handler.commitUserMessage({ parentId: 'does-not-exist' }),
|
|
177
|
+
).toThrow('Unknown parent message id: does-not-exist');
|
|
178
|
+
});
|
|
149
179
|
});
|
|
150
180
|
|
|
151
181
|
describe('updateMessage', () => {
|
|
@@ -342,6 +372,55 @@ describe('useMessageHandler', () => {
|
|
|
342
372
|
expect(handler.messages.value).toHaveLength(2);
|
|
343
373
|
expect(getText(handler.messages.value[1].content)).toBe('Streaming...');
|
|
344
374
|
});
|
|
375
|
+
|
|
376
|
+
it('branches a new node off an explicit parentId on create', () => {
|
|
377
|
+
const handler = useMessageHandler();
|
|
378
|
+
handler.setTree(
|
|
379
|
+
makeTree(
|
|
380
|
+
{
|
|
381
|
+
u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
|
|
382
|
+
a1: makeTreeMessage({
|
|
383
|
+
id: 'a1',
|
|
384
|
+
parentId: 'u1',
|
|
385
|
+
role: 'assistant',
|
|
386
|
+
content: [text('Hi')],
|
|
387
|
+
}),
|
|
388
|
+
},
|
|
389
|
+
'u1',
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Regenerate: add a sibling assistant answer under the same user turn.
|
|
394
|
+
handler.updateMessage({
|
|
395
|
+
id: 'a2',
|
|
396
|
+
content: [text('Hi again')],
|
|
397
|
+
role: 'assistant',
|
|
398
|
+
status: 'completed',
|
|
399
|
+
parentId: 'u1',
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const parent = handler.getTree().nodes.u1;
|
|
403
|
+
expect(parent.childrenIds).toEqual(['a1', 'a2']);
|
|
404
|
+
expect(parent.activeChildIndex).toBe(1);
|
|
405
|
+
expect(handler.messages.value.map((m) => m.id)).toEqual(['u1', 'a2']);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('ignores parentId when the node already exists', () => {
|
|
409
|
+
const handler = useMessageHandler();
|
|
410
|
+
handler.setCurrentUserMessage('Hello');
|
|
411
|
+
const msg = handler.commitUserMessage();
|
|
412
|
+
|
|
413
|
+
handler.updateMessage({
|
|
414
|
+
id: msg.id,
|
|
415
|
+
content: [text('Updated')],
|
|
416
|
+
role: 'user',
|
|
417
|
+
status: 'completed',
|
|
418
|
+
parentId: 'does-not-exist',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(getText(handler.getMessageById(msg.id)!.content)).toBe('Updated');
|
|
422
|
+
expect(handler.getTree().nodes[msg.id].parentId).toBeNull();
|
|
423
|
+
});
|
|
345
424
|
});
|
|
346
425
|
|
|
347
426
|
describe('getMessageById', () => {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
addNode as treeAddNode,
|
|
4
4
|
createEmptyTree,
|
|
5
5
|
cutBranchAfter as treeCutBranchAfter,
|
|
6
|
+
cutBranchAt as treeCutBranchAt,
|
|
6
7
|
getLinearPath,
|
|
7
8
|
navigate as treeNavigate,
|
|
8
9
|
updateNode,
|
|
@@ -30,6 +31,19 @@ export const useMessageHandler = () => {
|
|
|
30
31
|
return list.length > 0 ? list[list.length - 1].id : null;
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the parent a new node should hang off. When an explicit
|
|
36
|
+
* `parentId` is given it must reference an existing node; otherwise the
|
|
37
|
+
* current leaf is used so messages append to the active branch.
|
|
38
|
+
*/
|
|
39
|
+
function resolveParentId(parentId?: string): string | null {
|
|
40
|
+
if (parentId === undefined) return currentLeafId.value;
|
|
41
|
+
if (!tree.nodes[parentId]) {
|
|
42
|
+
throw new Error(`Unknown parent message id: ${parentId}`);
|
|
43
|
+
}
|
|
44
|
+
return parentId;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
function setCurrentUserMessage(text: string) {
|
|
34
48
|
currentUserMessage.value = text;
|
|
35
49
|
}
|
|
@@ -38,20 +52,31 @@ export const useMessageHandler = () => {
|
|
|
38
52
|
currentUserMessage.value = '';
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Commit the pending user message as a new node.
|
|
57
|
+
*
|
|
58
|
+
* By default the node is appended to the current leaf. Pass `parentId` to
|
|
59
|
+
* branch off an arbitrary message instead: the new node becomes a sibling
|
|
60
|
+
* of that parent's existing children, the parent's `childrenIds` gains the
|
|
61
|
+
* new id, and the branch is made active.
|
|
62
|
+
*/
|
|
41
63
|
function commitUserMessage(opts?: {
|
|
42
64
|
metadata?: MessageMetadata;
|
|
65
|
+
parentId?: string;
|
|
43
66
|
}): BaseMessage {
|
|
44
67
|
const id = generateId();
|
|
45
68
|
const text = currentUserMessage.value.trim();
|
|
46
69
|
if (!text) throw new Error('No message');
|
|
47
70
|
|
|
71
|
+
const parentId = resolveParentId(opts?.parentId);
|
|
72
|
+
|
|
48
73
|
const content: MessageContent[] = [
|
|
49
74
|
{ type: 'text', id: generateId(), text, status: 'completed' },
|
|
50
75
|
];
|
|
51
76
|
|
|
52
77
|
const node: TreeMessage = {
|
|
53
78
|
id,
|
|
54
|
-
parentId
|
|
79
|
+
parentId,
|
|
55
80
|
childrenIds: [],
|
|
56
81
|
activeChildIndex: 0,
|
|
57
82
|
role: 'user',
|
|
@@ -75,12 +100,20 @@ export const useMessageHandler = () => {
|
|
|
75
100
|
return base;
|
|
76
101
|
}
|
|
77
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Upsert a message by id. When the node does not yet exist it is created;
|
|
105
|
+
* pass `parentId` to branch the new node off an arbitrary message instead
|
|
106
|
+
* of the current leaf (the parent's `childrenIds` is updated and the new
|
|
107
|
+
* branch is made active). `parentId` is ignored when the node already
|
|
108
|
+
* exists.
|
|
109
|
+
*/
|
|
78
110
|
function updateMessage(message: {
|
|
79
111
|
id: string;
|
|
80
112
|
content: MessageContent[];
|
|
81
113
|
role: MessageRole;
|
|
82
114
|
status: MessageStatus;
|
|
83
115
|
metadata?: MessageMetadata;
|
|
116
|
+
parentId?: string;
|
|
84
117
|
}) {
|
|
85
118
|
const existing = tree.nodes[message.id];
|
|
86
119
|
|
|
@@ -96,7 +129,7 @@ export const useMessageHandler = () => {
|
|
|
96
129
|
} else {
|
|
97
130
|
const node: TreeMessage = {
|
|
98
131
|
id: message.id,
|
|
99
|
-
parentId:
|
|
132
|
+
parentId: resolveParentId(message.parentId),
|
|
100
133
|
childrenIds: [],
|
|
101
134
|
activeChildIndex: 0,
|
|
102
135
|
role: message.role,
|
|
@@ -159,6 +192,19 @@ export const useMessageHandler = () => {
|
|
|
159
192
|
treeCutBranchAfter(tree, messageId);
|
|
160
193
|
}
|
|
161
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Drop the message with `messageId` and its whole subtree, leaving sibling
|
|
197
|
+
* branches intact (the parent's active branch is clamped to a survivor).
|
|
198
|
+
* Use to discard one answer among several without cropping the others.
|
|
199
|
+
*/
|
|
200
|
+
function cutBranchAt(messageId: string) {
|
|
201
|
+
treeCutBranchAt(tree, messageId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getTree() {
|
|
205
|
+
return readonly(tree);
|
|
206
|
+
}
|
|
207
|
+
|
|
162
208
|
return {
|
|
163
209
|
messages: messages as Ref<BaseMessage[]>,
|
|
164
210
|
currentUserMessage: readonly(currentUserMessage),
|
|
@@ -173,5 +219,7 @@ export const useMessageHandler = () => {
|
|
|
173
219
|
setTree,
|
|
174
220
|
navigate,
|
|
175
221
|
cutBranchAfter,
|
|
222
|
+
cutBranchAt,
|
|
223
|
+
getTree,
|
|
176
224
|
};
|
|
177
225
|
};
|