@aleph-alpha/chat-kit 5.8.0 → 5.9.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-DG96wNug.js → CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js} +61 -40
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +46 -33
- package/dist/chat-kit.css +1 -1
- package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts +7 -0
- package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts.map +1 -1
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts +3 -1
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +23 -3
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +17 -2
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/adapters/responses-api.spec.ts +6 -3
- package/src/adapters/responses-api.ts +62 -35
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +209 -1
- package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +84 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +59 -21
- package/src/components/composed/CkConversation/CkConversation.spec.ts +96 -5
- package/src/components/composed/CkConversation/CkConversation.stories.ts +328 -4
- package/src/components/composed/CkConversation/CkConversation.vue +38 -8
|
@@ -15,6 +15,13 @@ export declare const Default: Story;
|
|
|
15
15
|
* Use when streaming tokens from an LLM into the last agent message.
|
|
16
16
|
*/
|
|
17
17
|
export declare const StreamingSimulation: Story;
|
|
18
|
+
/**
|
|
19
|
+
* Programmatic navigation via the exposed `scrollToMessage(index)` method. Enter
|
|
20
|
+
* a zero-based row index and the layout scrolls that message to the top. Index
|
|
21
|
+
* counts all rows regardless of role (a `thread-footer`, when present, is the
|
|
22
|
+
* trailing row); omitting the index scrolls to the last row.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ScrollToMessage: Story;
|
|
18
25
|
/**
|
|
19
26
|
* Empty state for when there are no messages. Use as an initial state before
|
|
20
27
|
* the first user turn, or when history has been cleared.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CkConversationLayout.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.stories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAI3C,QAAA,MAAM,oBAAoB,EACoB,eAAe,CAAC;AAoB9D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,oBAAoB,CAU3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"CkConversationLayout.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.stories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAI3C,QAAA,MAAM,oBAAoB,EACoB,eAAe,CAAC;AAoB9D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,oBAAoB,CAU3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA6GnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAsBrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAiFjC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,KA0C7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAenB,CAAC"}
|
|
@@ -7,7 +7,9 @@ declare const __VLS_export: <T extends {
|
|
|
7
7
|
}> & (typeof globalThis extends {
|
|
8
8
|
__VLS_PROPS_FALLBACK: infer P;
|
|
9
9
|
} ? P : {});
|
|
10
|
-
expose: (exposed: {
|
|
10
|
+
expose: (exposed: import("vue").ShallowUnwrapRef<{
|
|
11
|
+
scrollToMessage: (index?: number) => void;
|
|
12
|
+
}>) => void;
|
|
11
13
|
attrs: any;
|
|
12
14
|
slots: {
|
|
13
15
|
'user-message': (props: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CkConversationLayout.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.vue"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CkConversationLayout.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.vue"],"names":[],"mappings":"AA+TA,QAAA,MAAM,YAAY,GAAK,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5D,aAAa,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAC9D,YAAY,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,EAC3G,gBAAgB,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAClE;WAwYO,OAAO,KAAK,EAAE,WAAW,GAAG,mBAAmB;kBApY3C,CAAC,EAAE;MAoY2D,GAAG,CAAC,OAAO,UAAU,SAAS;QAAE,oBAAoB,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC1I,CAAC,OAAO,EAAE,OAAO,KAAK,EAAE,gBAAgB;kCAjPb,MAAM,KAAG,IAAI;MAiPsB,KAAK,IAAI;WACxE,GAAG;;wBAjYO,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;yBAC9B,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;QAChD;;;;WAIG;yBACc,MAAM,IAAI;;UA4XtB,EAAE;EAEL,KACQ,OAAO,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,WAAW,CAAC,CAAA;CAAI,CAAC;wBACvD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,CAAC,GAAG,EAAE,CAAC"}
|
|
@@ -12,6 +12,13 @@ type Story = StoryObj<typeof meta>;
|
|
|
12
12
|
* `#agent-message-header-content`.
|
|
13
13
|
*/
|
|
14
14
|
export declare const Default: Story;
|
|
15
|
+
/**
|
|
16
|
+
* A diagram of how CkConversation's slots nest. Every slot has a default
|
|
17
|
+
* rendering; overriding a parent slot replaces everything nested beneath it,
|
|
18
|
+
* so the child slots below it no longer apply. Collapse a node to see which
|
|
19
|
+
* slots "fold away" when you take over its parent.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SlotAnatomy: Story;
|
|
15
22
|
/**
|
|
16
23
|
* Uses a remote avatar image (with fallback text). Use when you want a
|
|
17
24
|
* branded agent identity in each reply.
|
|
@@ -70,11 +77,24 @@ export declare const StreamingSimulation: Story;
|
|
|
70
77
|
*/
|
|
71
78
|
export declare const WithCustomMarkdownClasses: Story;
|
|
72
79
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
80
|
+
* Fenced code blocks in agent markdown render with `UiCodeBlock` out of the
|
|
81
|
+
* box — syntax highlighting, a language label, and a copy button — without any
|
|
82
|
+
* slot wiring.
|
|
83
|
+
*/
|
|
84
|
+
export declare const DefaultCodeBlock: Story;
|
|
85
|
+
/**
|
|
86
|
+
* Fenced code blocks render with `UiCodeBlock` (syntax highlighting + copy
|
|
87
|
+
* button) by default. Override that with a custom renderer (Prism, a bespoke
|
|
88
|
+
* theme, etc.) via `#agent-message-code-block`, as shown here.
|
|
76
89
|
*/
|
|
77
90
|
export declare const WithCustomCodeBlock: Story;
|
|
91
|
+
/**
|
|
92
|
+
* Programmatic navigation via the exposed `scrollToMessage(index)` method. Enter
|
|
93
|
+
* a zero-based row index and the conversation scrolls that message to the top.
|
|
94
|
+
* Index counts all rows regardless of role (a `thread-footer`, when present, is
|
|
95
|
+
* the trailing row); omitting the index scrolls to the last row.
|
|
96
|
+
*/
|
|
97
|
+
export declare const ScrollToMessage: Story;
|
|
78
98
|
/**
|
|
79
99
|
* Empty state — an initial view before the first turn. Use to show a welcome
|
|
80
100
|
* screen, quick prompts, or onboarding content above this surface.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CkConversation.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.stories.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAwI3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAkCrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"CkConversation.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.stories.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAwI3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAkCrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA6WnC;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,KAerB,CAAC;AA2HF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,KAgCzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,KAYtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,KASpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,KAe5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,KAU7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAmC/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAiBnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,KAiBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KA4GjC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,KAmBvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,KA6C9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAoDjC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,KAsC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC"}
|
|
@@ -49,7 +49,11 @@ type __VLS_Slots = {
|
|
|
49
49
|
'agent-message-footer': (props: {
|
|
50
50
|
message: BaseMessage;
|
|
51
51
|
}) => void;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Replace the default code block rendering inside agent markdown. When not
|
|
54
|
+
* provided, fenced code blocks render with `UiCodeBlock` (syntax highlighting
|
|
55
|
+
* + copy button).
|
|
56
|
+
*/
|
|
53
57
|
'agent-message-code-block': (props: {
|
|
54
58
|
code: string;
|
|
55
59
|
language: string;
|
|
@@ -61,7 +65,18 @@ type __VLS_Slots = {
|
|
|
61
65
|
*/
|
|
62
66
|
'thread-footer': () => void;
|
|
63
67
|
};
|
|
64
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Scroll so the message at `index` sits at the top of the viewport. `index` is
|
|
70
|
+
* the zero-based position among the rendered rows (any `thread-footer` counts
|
|
71
|
+
* as the trailing row); when omitted it scrolls to the last row.
|
|
72
|
+
*
|
|
73
|
+
* This only performs the scroll — it does not focus the message. Any
|
|
74
|
+
* accompanying focus handling must be done separately by the caller.
|
|
75
|
+
*/
|
|
76
|
+
declare function scrollToMessage(index?: number): void;
|
|
77
|
+
declare const __VLS_base: import("vue").DefineComponent<CkConversationProps, {
|
|
78
|
+
scrollToMessage: typeof scrollToMessage;
|
|
79
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<CkConversationProps> & Readonly<{}>, {
|
|
65
80
|
agentAvatarImage: string;
|
|
66
81
|
agentAvatarFallback: string;
|
|
67
82
|
agentLoadingImage: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CkConversation.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.vue"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CkConversation.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.vue"],"names":[],"mappings":"AAqRA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,gBAAgB,CAAC;AAOtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA0BnD,KAAK,WAAW,GAAG;IACjB,gDAAgD;IAChD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1D,yDAAyD;IACzD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,iDAAiD;IACjD,eAAe,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,oCAAoC;IACpC,oBAAoB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,8EAA8E;IAC9E,4BAA4B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,iFAAiF;IACjF,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,6EAA6E;IAC7E,8BAA8B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1E;;;;OAIG;IACH,+BAA+B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3E;;;;OAIG;IACH,uBAAuB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,mDAAmD;IACnD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;OAIG;IACH,0BAA0B,EAAE,CAAC,KAAK,EAAE;QAClC,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,IAAI,CAAC;IACX;;;;OAIG;IACH,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AA8DF;;;;;;;GAOG;AACH,iBAAS,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAE7C;AAkVD,QAAA,MAAM,UAAU;;;;;;6EAId,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AAWzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
|
|
@@ -40,10 +40,10 @@ declare const __VLS_base: import("vue").DefineComponent<CkHistoryProps, {}, {},
|
|
|
40
40
|
}>, {
|
|
41
41
|
showAction: boolean;
|
|
42
42
|
showActionOnHover: boolean;
|
|
43
|
+
maxHeight: string;
|
|
43
44
|
activeId: string;
|
|
44
45
|
timeFormatter: (updatedAtSeconds: number) => string;
|
|
45
46
|
collapsedLimit: number;
|
|
46
|
-
maxHeight: string;
|
|
47
47
|
contentClass: string;
|
|
48
48
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
49
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
package/dist/components/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _, a, C, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z } from "../CkHistory.vue_vue_type_script_setup_true_lang-
|
|
1
|
+
import { _, a, C, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z } from "../CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js";
|
|
2
2
|
export {
|
|
3
3
|
_ as CkAgentMessage,
|
|
4
4
|
a as CkAgentMessageHeader,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { u } from "./useMessageHandler-Tellov3w.js";
|
|
2
2
|
import { C, d, p, u as u2, b } from "./useChatKitLabels-CMOr3Kjq.js";
|
|
3
3
|
import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-Dn4ctxk8.js";
|
|
4
|
-
import { _, a as a2, C as C2, b as b3, c as c2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A } from "./CkHistory.vue_vue_type_script_setup_true_lang-
|
|
4
|
+
import { _, a as a2, C as C2, b as b3, c as c2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A } from "./CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js";
|
|
5
5
|
export {
|
|
6
6
|
C as CHAT_KIT_LABELS,
|
|
7
7
|
_ as CkAgentMessage,
|
package/package.json
CHANGED
|
@@ -456,9 +456,12 @@ describe('responsesToTree', () => {
|
|
|
456
456
|
},
|
|
457
457
|
],
|
|
458
458
|
});
|
|
459
|
-
// The assistant turn does not inherit attachments but still carries
|
|
460
|
-
//
|
|
461
|
-
expect(tree.nodes['msg-a1']!.metadata).toEqual({
|
|
459
|
+
// The assistant turn does not inherit attachments but still carries its
|
|
460
|
+
// responseId stamp and the response's model id.
|
|
461
|
+
expect(tree.nodes['msg-a1']!.metadata).toEqual({
|
|
462
|
+
responseId: 'r1',
|
|
463
|
+
model: 'pharia-1',
|
|
464
|
+
});
|
|
462
465
|
});
|
|
463
466
|
|
|
464
467
|
it('skips malformed metadata.attachments without polluting the tree', () => {
|
|
@@ -462,6 +462,53 @@ function parseResponseAttachments(
|
|
|
462
462
|
return attachments as ResponseAttachment[];
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// Maps response-level attachments onto the shape stored in node metadata.
|
|
466
|
+
function toMessageAttachments(
|
|
467
|
+
attachments: ResponseAttachment[],
|
|
468
|
+
): ResponseMessageAttachment[] {
|
|
469
|
+
return attachments.map(
|
|
470
|
+
(a): ResponseMessageAttachment => ({
|
|
471
|
+
name: a.fileName,
|
|
472
|
+
fileId: a.fileId,
|
|
473
|
+
fileName: a.fileName,
|
|
474
|
+
searchStoreId: a.searchStoreId,
|
|
475
|
+
...(a.documentId ? { documentId: a.documentId } : {}),
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Builds the user node for a single `request_input`, or null when the input is
|
|
481
|
+
// not a user turn / carries no visible content. Attachments bind to the first
|
|
482
|
+
// user node only (`isFirstUserNode`).
|
|
483
|
+
function buildUserNode(
|
|
484
|
+
resp: StoredResponse,
|
|
485
|
+
input: unknown,
|
|
486
|
+
idx: number,
|
|
487
|
+
responseAttachments: ResponseAttachment[] | null,
|
|
488
|
+
isFirstUserNode: boolean,
|
|
489
|
+
): ResponseMessageNode | null {
|
|
490
|
+
const role = (input as { role?: string }).role;
|
|
491
|
+
const text = extractInputText((input as { content?: unknown }).content);
|
|
492
|
+
const hasContent = text || responseAttachments?.length;
|
|
493
|
+
if (role !== 'user' || !hasContent) return null;
|
|
494
|
+
|
|
495
|
+
const metadata: ResponseTreeMessageMetadata = {
|
|
496
|
+
responseId: resp.id,
|
|
497
|
+
...(resp.model ? { model: resp.model } : {}),
|
|
498
|
+
};
|
|
499
|
+
if (responseAttachments && isFirstUserNode) {
|
|
500
|
+
metadata.attachments = toMessageAttachments(responseAttachments);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
id: `user-${resp.id}-${idx}`,
|
|
505
|
+
role: 'user',
|
|
506
|
+
content: [textPart(text)],
|
|
507
|
+
createdAt: new Date(resp.created_at * 1000),
|
|
508
|
+
metadata,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
465
512
|
// Within a single response, the visible messages chain linearly: every user
|
|
466
513
|
// input is followed by exactly one assistant turn (whose reasoning, tool,
|
|
467
514
|
// and text outputs are merged into one node). Across responses, branching
|
|
@@ -473,40 +520,15 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
|
|
|
473
520
|
// the user turn that started the response. We bind them to the first
|
|
474
521
|
// user `request_input` (mirroring the existing wire convention).
|
|
475
522
|
const responseAttachments = parseResponseAttachments(resp);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const metadata: ResponseTreeMessageMetadata = {
|
|
486
|
-
responseId: resp.id,
|
|
487
|
-
...(resp.model ? { model: resp.model } : {}),
|
|
488
|
-
};
|
|
489
|
-
if (responseAttachments && userNodesPushed === 0) {
|
|
490
|
-
metadata.attachments = responseAttachments.map(
|
|
491
|
-
(a): ResponseMessageAttachment => ({
|
|
492
|
-
name: a.fileName,
|
|
493
|
-
fileId: a.fileId,
|
|
494
|
-
fileName: a.fileName,
|
|
495
|
-
searchStoreId: a.searchStoreId,
|
|
496
|
-
...(a.documentId ? { documentId: a.documentId } : {}),
|
|
497
|
-
}),
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
nodes.push({
|
|
502
|
-
id: `user-${resp.id}-${idx}`,
|
|
503
|
-
role: 'user',
|
|
504
|
-
content: [textPart(text)],
|
|
505
|
-
createdAt: new Date(resp.created_at * 1000),
|
|
506
|
-
metadata,
|
|
507
|
-
});
|
|
508
|
-
userNodesPushed += 1;
|
|
509
|
-
}
|
|
523
|
+
for (const [idx, input] of resp.request_input?.entries() ?? []) {
|
|
524
|
+
const userNode = buildUserNode(
|
|
525
|
+
resp,
|
|
526
|
+
input,
|
|
527
|
+
idx,
|
|
528
|
+
responseAttachments,
|
|
529
|
+
nodes.length === 0,
|
|
530
|
+
);
|
|
531
|
+
if (userNode) nodes.push(userNode);
|
|
510
532
|
}
|
|
511
533
|
|
|
512
534
|
const assistantParts = toAssistantParts(resp);
|
|
@@ -516,7 +538,12 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
|
|
|
516
538
|
role: 'assistant',
|
|
517
539
|
content: assistantParts,
|
|
518
540
|
createdAt: new Date((resp.completed_at ?? resp.created_at) * 1000),
|
|
519
|
-
|
|
541
|
+
// A stored response carries a single `model` covering both its input and
|
|
542
|
+
// output, so the assistant node gets the same model id as its user node.
|
|
543
|
+
metadata: {
|
|
544
|
+
responseId: resp.id,
|
|
545
|
+
...(resp.model ? { model: resp.model } : {}),
|
|
546
|
+
},
|
|
520
547
|
});
|
|
521
548
|
}
|
|
522
549
|
return nodes;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import CkConversationLayout from './CkConversationLayout.vue';
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/vue';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type Component,
|
|
5
|
+
defineComponent,
|
|
6
|
+
h,
|
|
7
|
+
nextTick,
|
|
8
|
+
onMounted,
|
|
9
|
+
ref,
|
|
10
|
+
} from 'vue';
|
|
11
|
+
|
|
12
|
+
type LayoutInstance = { scrollToMessage: (index?: number) => void };
|
|
4
13
|
|
|
5
14
|
const messages = [
|
|
6
15
|
{ role: 'user', content: 'Hello there' },
|
|
@@ -37,6 +46,46 @@ function renderLayout(
|
|
|
37
46
|
return render(Wrapper);
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Render the layout and capture the child component instance so the exposed
|
|
51
|
+
* `scrollToMessage` method can be invoked from the test.
|
|
52
|
+
*/
|
|
53
|
+
function renderWithInstance(
|
|
54
|
+
msgs: Array<{ role: string; content: string }> = messages,
|
|
55
|
+
{ withFooter = false }: { withFooter?: boolean } = {},
|
|
56
|
+
) {
|
|
57
|
+
let instance: LayoutInstance | undefined;
|
|
58
|
+
// eslint-disable-next-line vue/one-component-per-file
|
|
59
|
+
const Wrapper = defineComponent({
|
|
60
|
+
setup() {
|
|
61
|
+
const layoutRef = ref<LayoutInstance>();
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
instance = layoutRef.value;
|
|
64
|
+
});
|
|
65
|
+
return () =>
|
|
66
|
+
h(
|
|
67
|
+
CkConversationLayout as unknown as Component,
|
|
68
|
+
{ messages: msgs, ref: layoutRef },
|
|
69
|
+
{
|
|
70
|
+
'user-message': ({ message }: { message: (typeof msgs)[0] }) =>
|
|
71
|
+
h('p', { 'data-testid': 'user-msg' }, message.content),
|
|
72
|
+
'agent-message': ({ message }: { message: (typeof msgs)[0] }) =>
|
|
73
|
+
h('p', { 'data-testid': 'agent-msg' }, message.content),
|
|
74
|
+
...(withFooter
|
|
75
|
+
? {
|
|
76
|
+
'thread-footer': () =>
|
|
77
|
+
h('p', { 'data-testid': 'footer' }, 'footer'),
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const utils = render(Wrapper);
|
|
86
|
+
return { ...utils, getInstance: () => instance! };
|
|
87
|
+
}
|
|
88
|
+
|
|
40
89
|
describe('CkConversationLayout', () => {
|
|
41
90
|
describe('message display', () => {
|
|
42
91
|
it('displays all provided messages', () => {
|
|
@@ -127,6 +176,90 @@ describe('CkConversationLayout', () => {
|
|
|
127
176
|
});
|
|
128
177
|
});
|
|
129
178
|
|
|
179
|
+
describe('row spacing', () => {
|
|
180
|
+
it('gives the first row a smaller top margin than the following rows', () => {
|
|
181
|
+
const { container } = renderLayout();
|
|
182
|
+
|
|
183
|
+
const rows =
|
|
184
|
+
container.querySelectorAll<HTMLElement>('[data-message-row]');
|
|
185
|
+
expect(rows[0].className).toContain('mt-2xl');
|
|
186
|
+
expect(rows[0].className).not.toContain('mt-3xl');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('gives every non-first row the larger top margin', () => {
|
|
190
|
+
const { container } = renderLayout();
|
|
191
|
+
|
|
192
|
+
const rows =
|
|
193
|
+
container.querySelectorAll<HTMLElement>('[data-message-row]');
|
|
194
|
+
for (const row of Array.from(rows).slice(1)) {
|
|
195
|
+
expect(row.className).toContain('mt-3xl');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not use a flex gap for row spacing', () => {
|
|
200
|
+
const { container } = renderLayout();
|
|
201
|
+
|
|
202
|
+
const scrollEl = container.querySelector(
|
|
203
|
+
'[class*="overflow-y-auto"]',
|
|
204
|
+
) as HTMLElement;
|
|
205
|
+
expect(scrollEl.className).not.toContain('gap-8');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('gives the thread-footer the larger top margin when messages exist', () => {
|
|
209
|
+
// eslint-disable-next-line vue/one-component-per-file
|
|
210
|
+
const Wrapper = defineComponent({
|
|
211
|
+
setup() {
|
|
212
|
+
return () =>
|
|
213
|
+
h(
|
|
214
|
+
CkConversationLayout as unknown as Component,
|
|
215
|
+
{ messages },
|
|
216
|
+
{
|
|
217
|
+
'user-message': () => h('p', 'user'),
|
|
218
|
+
'agent-message': () => h('p', 'agent'),
|
|
219
|
+
'thread-footer': () =>
|
|
220
|
+
h('p', { 'data-testid': 'footer' }, 'footer'),
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const { container } = render(Wrapper);
|
|
227
|
+
|
|
228
|
+
const footerRow = container.querySelector(
|
|
229
|
+
'[data-message-row="agent"]:last-of-type',
|
|
230
|
+
) as HTMLElement;
|
|
231
|
+
expect(footerRow.querySelector('[data-testid="footer"]')).not.toBeNull();
|
|
232
|
+
expect(footerRow.className).toContain('mt-3xl');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('gives the thread-footer the smaller top margin when there are no messages', () => {
|
|
236
|
+
// eslint-disable-next-line vue/one-component-per-file
|
|
237
|
+
const Wrapper = defineComponent({
|
|
238
|
+
setup() {
|
|
239
|
+
return () =>
|
|
240
|
+
h(
|
|
241
|
+
CkConversationLayout as unknown as Component,
|
|
242
|
+
{ messages: [] },
|
|
243
|
+
{
|
|
244
|
+
'user-message': () => h('p', 'user'),
|
|
245
|
+
'agent-message': () => h('p', 'agent'),
|
|
246
|
+
'thread-footer': () =>
|
|
247
|
+
h('p', { 'data-testid': 'footer' }, 'footer'),
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const { container } = render(Wrapper);
|
|
254
|
+
|
|
255
|
+
const footerRow = container.querySelector(
|
|
256
|
+
'[data-message-row="agent"]',
|
|
257
|
+
) as HTMLElement;
|
|
258
|
+
expect(footerRow.className).toContain('mt-2xl');
|
|
259
|
+
expect(footerRow.className).not.toContain('mt-3xl');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
130
263
|
describe('scroll fade indicators', () => {
|
|
131
264
|
function getFadeElements(container: HTMLElement) {
|
|
132
265
|
return {
|
|
@@ -209,4 +342,79 @@ describe('CkConversationLayout', () => {
|
|
|
209
342
|
expect(bottomFade.className).toContain('opacity-0');
|
|
210
343
|
});
|
|
211
344
|
});
|
|
345
|
+
|
|
346
|
+
describe('scrollToMessage', () => {
|
|
347
|
+
// jsdom reports offsetTop as 0, so stub distinct positions per rendered row.
|
|
348
|
+
function stubRowOffsets(container: Element) {
|
|
349
|
+
container
|
|
350
|
+
.querySelectorAll<HTMLElement>('[data-message-row]')
|
|
351
|
+
.forEach((row, i) => {
|
|
352
|
+
Object.defineProperty(row, 'offsetTop', {
|
|
353
|
+
configurable: true,
|
|
354
|
+
value: (i + 1) * 100,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function setup(options?: { withFooter?: boolean }) {
|
|
360
|
+
const { container, getInstance } = renderWithInstance(messages, options);
|
|
361
|
+
const scrollEl = container.querySelector(
|
|
362
|
+
'[class*="overflow-y-auto"]',
|
|
363
|
+
) as HTMLElement;
|
|
364
|
+
const scrollTo = vi.fn();
|
|
365
|
+
scrollEl.scrollTo = scrollTo;
|
|
366
|
+
stubRowOffsets(container);
|
|
367
|
+
|
|
368
|
+
return { getInstance, scrollTo };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
it('scrolls to the last message by default', async () => {
|
|
372
|
+
// Four rows (user, agent, user, agent) → last row is at offsetTop 400.
|
|
373
|
+
const { getInstance, scrollTo } = setup();
|
|
374
|
+
|
|
375
|
+
getInstance().scrollToMessage();
|
|
376
|
+
await nextTick();
|
|
377
|
+
|
|
378
|
+
expect(scrollTo).toHaveBeenCalledWith(
|
|
379
|
+
expect.objectContaining({ top: 400, behavior: 'smooth' }),
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('scrolls to the message at the given index, regardless of role', async () => {
|
|
384
|
+
const { getInstance, scrollTo } = setup();
|
|
385
|
+
|
|
386
|
+
getInstance().scrollToMessage(1);
|
|
387
|
+
await nextTick();
|
|
388
|
+
|
|
389
|
+
expect(scrollTo).toHaveBeenCalledWith(
|
|
390
|
+
expect.objectContaining({ top: 200, behavior: 'smooth' }),
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('treats the thread-footer as the trailing row', async () => {
|
|
395
|
+
// Four messages + footer → the footer is the fifth row at offsetTop 500.
|
|
396
|
+
const { getInstance, scrollTo } = setup({ withFooter: true });
|
|
397
|
+
|
|
398
|
+
getInstance().scrollToMessage();
|
|
399
|
+
await nextTick();
|
|
400
|
+
|
|
401
|
+
expect(scrollTo).toHaveBeenCalledWith(
|
|
402
|
+
expect.objectContaining({ top: 500, behavior: 'smooth' }),
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('does nothing when there are no messages', async () => {
|
|
407
|
+
const { container, getInstance } = renderWithInstance([]);
|
|
408
|
+
const scrollEl = container.querySelector(
|
|
409
|
+
'[class*="overflow-y-auto"]',
|
|
410
|
+
) as HTMLElement;
|
|
411
|
+
const scrollTo = vi.fn();
|
|
412
|
+
scrollEl.scrollTo = scrollTo;
|
|
413
|
+
|
|
414
|
+
getInstance().scrollToMessage();
|
|
415
|
+
await nextTick();
|
|
416
|
+
|
|
417
|
+
expect(scrollTo).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
212
420
|
});
|
|
@@ -99,6 +99,40 @@ function streamAgentReply(): void {
|
|
|
99
99
|
</div>
|
|
100
100
|
</template>`;
|
|
101
101
|
|
|
102
|
+
const scrollToMessageTemplateSource = `<script setup lang="ts">
|
|
103
|
+
import { CkConversationLayout } from '@aleph-alpha/chat-kit';
|
|
104
|
+
import { ref, useTemplateRef } from 'vue';
|
|
105
|
+
|
|
106
|
+
interface Message { role: string; content: string }
|
|
107
|
+
const messages: Message[] = [/* ... */];
|
|
108
|
+
|
|
109
|
+
const layout = useTemplateRef('layout');
|
|
110
|
+
const index = ref(0);
|
|
111
|
+
|
|
112
|
+
function scrollToMessage(): void {
|
|
113
|
+
// Zero-based index among all rows (a thread-footer counts as the last row).
|
|
114
|
+
layout.value?.scrollToMessage(index.value);
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<template>
|
|
119
|
+
<div class="flex h-[400px] flex-col">
|
|
120
|
+
<div class="flex items-center gap-2 border-b p-2">
|
|
121
|
+
<label for="msg-index">Message index</label>
|
|
122
|
+
<input id="msg-index" v-model.number="index" type="number" min="0" />
|
|
123
|
+
<button @click="scrollToMessage">Scroll to message</button>
|
|
124
|
+
</div>
|
|
125
|
+
<CkConversationLayout ref="layout" :messages="messages" class="min-h-0 flex-1">
|
|
126
|
+
<template #user-message="{ message }">
|
|
127
|
+
<div class="bg-core-bg-secondary rounded-md px-3 py-2 max-w-[400px]">{{ message.content }}</div>
|
|
128
|
+
</template>
|
|
129
|
+
<template #agent-message="{ message }">
|
|
130
|
+
<div class="px-3 py-2 max-w-[400px]">{{ message.content }}</div>
|
|
131
|
+
</template>
|
|
132
|
+
</CkConversationLayout>
|
|
133
|
+
</div>
|
|
134
|
+
</template>`;
|
|
135
|
+
|
|
102
136
|
const emptyTemplateSource = `<script setup lang="ts">
|
|
103
137
|
import { CkConversationLayout } from '@aleph-alpha/chat-kit';
|
|
104
138
|
|
|
@@ -229,6 +263,56 @@ export const StreamingSimulation: Story = {
|
|
|
229
263
|
parameters: { docs: { source: { code: streamingSimulationTemplateSource } } },
|
|
230
264
|
};
|
|
231
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Programmatic navigation via the exposed `scrollToMessage(index)` method. Enter
|
|
268
|
+
* a zero-based row index and the layout scrolls that message to the top. Index
|
|
269
|
+
* counts all rows regardless of role (a `thread-footer`, when present, is the
|
|
270
|
+
* trailing row); omitting the index scrolls to the last row.
|
|
271
|
+
*/
|
|
272
|
+
export const ScrollToMessage: Story = {
|
|
273
|
+
render: () => ({
|
|
274
|
+
components: { CkConversationLayout },
|
|
275
|
+
setup() {
|
|
276
|
+
const messages = Array.from({ length: 12 }, (_, i) => ({
|
|
277
|
+
role: i % 2 === 0 ? 'user' : 'agent',
|
|
278
|
+
content: `[${i}] ${
|
|
279
|
+
i % 2 === 0 ? 'User' : 'Agent'
|
|
280
|
+
} message number ${i}. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.`,
|
|
281
|
+
}));
|
|
282
|
+
const layout = ref();
|
|
283
|
+
const index = ref(0);
|
|
284
|
+
function scrollToMessage() {
|
|
285
|
+
layout.value?.scrollToMessage(index.value);
|
|
286
|
+
}
|
|
287
|
+
return { messages, layout, index, scrollToMessage };
|
|
288
|
+
},
|
|
289
|
+
template: `
|
|
290
|
+
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
291
|
+
<div style="display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid #e0e0e0;">
|
|
292
|
+
<label for="msg-index">Message index</label>
|
|
293
|
+
<input id="msg-index" v-model.number="index" type="number" min="0" style="width: 72px; padding: 4px 8px; border: 1px solid #ccc; border-radius: 6px;" />
|
|
294
|
+
<button @click="scrollToMessage" style="padding: 6px 16px; border-radius: 6px; background: #333; color: white; cursor: pointer;">
|
|
295
|
+
Scroll to message
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
<CkConversationLayout ref="layout" :messages="messages" style="flex: 1; min-height: 0;">
|
|
299
|
+
<template #user-message="{ message }">
|
|
300
|
+
<div style="background: #f0f0f0; padding: 8px 12px; border-radius: 12px 0 12px 12px; max-width: 400px;">
|
|
301
|
+
{{ message.content }}
|
|
302
|
+
</div>
|
|
303
|
+
</template>
|
|
304
|
+
<template #agent-message="{ message }">
|
|
305
|
+
<div style="padding: 8px 12px; max-width: 400px;">
|
|
306
|
+
{{ message.content }}
|
|
307
|
+
</div>
|
|
308
|
+
</template>
|
|
309
|
+
</CkConversationLayout>
|
|
310
|
+
</div>
|
|
311
|
+
`,
|
|
312
|
+
}),
|
|
313
|
+
parameters: { docs: { source: { code: scrollToMessageTemplateSource } } },
|
|
314
|
+
};
|
|
315
|
+
|
|
232
316
|
/**
|
|
233
317
|
* Empty state for when there are no messages. Use as an initial state before
|
|
234
318
|
* the first user turn, or when history has been cleared.
|