@copilotkit/vue 1.81.0 → 1.83.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/AGENTS.md +8 -7
- package/CONTRIBUTOR_GUIDE.md +120 -0
- package/PARITY.md +19 -10
- package/README.md +24 -373
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/styles.css +1 -1
- package/dist/use-render-activity-message-B7sDGUl4.cjs +70 -0
- package/dist/use-render-activity-message-B7sDGUl4.cjs.map +1 -0
- package/dist/{use-render-activity-message-5H6AfuJB.js → use-render-activity-message-mQvByEYF.js} +3189 -3147
- package/dist/use-render-activity-message-mQvByEYF.js.map +1 -0
- package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -0
- package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
- package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
- package/dist/v2/components/a2ui/utils.d.ts +1 -1
- package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +54 -0
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts +3 -0
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts +4 -0
- package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/providers/CopilotKitProvider.types.d.ts +15 -0
- package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/types.d.ts +9 -0
- package/dist/v2/providers/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/styles/globals.css +7 -0
- package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +188 -0
- package/src/v2/components/__tests__/A2UICatalogContext.test.ts +63 -0
- package/src/v2/components/a2ui/A2UICatalogContext.ts +14 -2
- package/src/v2/components/a2ui/catalog.ts +44 -15
- package/src/v2/components/a2ui/utils.ts +1 -1
- package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +8 -3
- package/src/v2/components/chat/CopilotChatInput.vue +2 -2
- package/src/v2/components/chat/CopilotChatReasoningMessage.vue +11 -1
- package/src/v2/components/chat/CopilotModalHeader.vue +4 -1
- package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +11 -1
- package/src/v2/components/chat/CopilotSidebarViewInternal.vue +3 -1
- package/src/v2/components/chat/__tests__/CopilotChat.labels.test.ts +198 -0
- package/src/v2/providers/CopilotKitProvider.types.ts +15 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +50 -0
- package/src/v2/providers/types.ts +9 -0
- package/dist/use-render-activity-message-5H6AfuJB.js.map +0 -1
- package/dist/use-render-activity-message-DxOdM2Qt.cjs +0 -70
- package/dist/use-render-activity-message-DxOdM2Qt.cjs.map +0 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Catalog } from "@a2ui/web_core/v0_9";
|
|
4
|
+
import type { ComponentApi } from "@a2ui/web_core/v0_9";
|
|
5
|
+
import { buildCatalogContextValue } from "../a2ui/A2UICatalogContext";
|
|
6
|
+
import { vueBasicCatalog } from "../a2ui/catalog";
|
|
7
|
+
|
|
8
|
+
function makeCustomCatalog(): Catalog<ComponentApi> {
|
|
9
|
+
const components: ComponentApi[] = [
|
|
10
|
+
{ name: "FlightCard", schema: z.object({ airline: z.string() }) },
|
|
11
|
+
{
|
|
12
|
+
name: "PieChart",
|
|
13
|
+
schema: z.object({ innerRadius: z.number().optional() }),
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
return new Catalog<ComponentApi>(
|
|
17
|
+
"copilotkit://custom-catalog",
|
|
18
|
+
components,
|
|
19
|
+
[],
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("buildCatalogContextValue", () => {
|
|
24
|
+
it("includes each custom component's JSON schema by default", () => {
|
|
25
|
+
const value = buildCatalogContextValue(makeCustomCatalog());
|
|
26
|
+
|
|
27
|
+
expect(value).toContain("- copilotkit://custom-catalog");
|
|
28
|
+
expect(value).toContain(" - FlightCard:");
|
|
29
|
+
expect(value).toContain('"airline"');
|
|
30
|
+
expect(value).toContain('"innerRadius"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("includes schemas when includeSchema is true", () => {
|
|
34
|
+
const catalog = makeCustomCatalog();
|
|
35
|
+
|
|
36
|
+
expect(buildCatalogContextValue(catalog, { includeSchema: true })).toBe(
|
|
37
|
+
buildCatalogContextValue(catalog),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("lists custom components by name only when includeSchema is false", () => {
|
|
42
|
+
const value = buildCatalogContextValue(makeCustomCatalog(), {
|
|
43
|
+
includeSchema: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(value).toBe(
|
|
47
|
+
[
|
|
48
|
+
"Available A2UI catalog:",
|
|
49
|
+
"- copilotkit://custom-catalog",
|
|
50
|
+
" Custom catalog (does NOT include all basic components).",
|
|
51
|
+
" Custom components:",
|
|
52
|
+
" - FlightCard",
|
|
53
|
+
" - PieChart",
|
|
54
|
+
].join("\n"),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("describes the basic catalog the same way regardless of includeSchema", () => {
|
|
59
|
+
expect(
|
|
60
|
+
buildCatalogContextValue(vueBasicCatalog, { includeSchema: false }),
|
|
61
|
+
).toBe(buildCatalogContextValue(vueBasicCatalog));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -25,8 +25,14 @@ const A2UI_SCHEMA_CONTEXT_DESCRIPTION =
|
|
|
25
25
|
/**
|
|
26
26
|
* Build a context string describing the available A2UI catalog.
|
|
27
27
|
* Vue-native equivalent of buildCatalogContextValue from a2ui-renderer.
|
|
28
|
+
*
|
|
29
|
+
* When `options.includeSchema` is `false`, custom components are listed by name
|
|
30
|
+
* only and their JSON schemas are omitted.
|
|
28
31
|
*/
|
|
29
|
-
function buildCatalogContextValue(
|
|
32
|
+
export function buildCatalogContextValue(
|
|
33
|
+
catalog?: Catalog<ComponentApi>,
|
|
34
|
+
options?: { includeSchema?: boolean },
|
|
35
|
+
): string {
|
|
30
36
|
const resolved = catalog ?? vueBasicCatalog;
|
|
31
37
|
const BASIC_CATALOG_ID =
|
|
32
38
|
"https://a2ui.org/specification/v0_9/basic_catalog.json";
|
|
@@ -68,6 +74,10 @@ function buildCatalogContextValue(catalog?: Catalog<ComponentApi>): string {
|
|
|
68
74
|
for (const name of customNames) {
|
|
69
75
|
const comp = resolved.components.get(name);
|
|
70
76
|
if (!comp) continue;
|
|
77
|
+
if (options?.includeSchema === false) {
|
|
78
|
+
lines.push(` - ${name}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
71
81
|
const jsonSchema = zodToJsonSchema(comp.schema);
|
|
72
82
|
lines.push(` - ${name}:`);
|
|
73
83
|
lines.push(
|
|
@@ -127,7 +137,9 @@ export function registerA2UICatalogContext(
|
|
|
127
137
|
},
|
|
128
138
|
): void {
|
|
129
139
|
const contextValue = computed(() =>
|
|
130
|
-
buildCatalogContextValue(options.catalog()
|
|
140
|
+
buildCatalogContextValue(options.catalog(), {
|
|
141
|
+
includeSchema: options.includeSchema(),
|
|
142
|
+
}),
|
|
131
143
|
);
|
|
132
144
|
|
|
133
145
|
// Register catalog capabilities context
|
|
@@ -88,7 +88,13 @@ const Text = createVueComponent(TextApi, ({ props }) => {
|
|
|
88
88
|
case "caption":
|
|
89
89
|
return h(
|
|
90
90
|
"small",
|
|
91
|
-
{
|
|
91
|
+
{
|
|
92
|
+
style: {
|
|
93
|
+
...style,
|
|
94
|
+
color: "var(--a2ui-color-muted, #666)",
|
|
95
|
+
textAlign: "left",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
92
98
|
text,
|
|
93
99
|
);
|
|
94
100
|
case "body":
|
|
@@ -175,7 +181,12 @@ const AudioPlayer = createVueComponent(AudioPlayerApi, ({ props }) => {
|
|
|
175
181
|
props.description
|
|
176
182
|
? h(
|
|
177
183
|
"span",
|
|
178
|
-
{
|
|
184
|
+
{
|
|
185
|
+
style: {
|
|
186
|
+
fontSize: "12px",
|
|
187
|
+
color: "var(--a2ui-color-muted, #666)",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
179
190
|
props.description,
|
|
180
191
|
)
|
|
181
192
|
: null,
|
|
@@ -239,7 +250,7 @@ const List = createVueComponent(ListApi, ({ props, buildChild }) => {
|
|
|
239
250
|
const Card = createVueComponent(CardApi, ({ props, buildChild }) => {
|
|
240
251
|
const style = {
|
|
241
252
|
...getBaseContainerStyle(),
|
|
242
|
-
backgroundColor: "#fff",
|
|
253
|
+
backgroundColor: "var(--a2ui-color-surface, #fff)",
|
|
243
254
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
244
255
|
width: "100%",
|
|
245
256
|
};
|
|
@@ -268,7 +279,7 @@ const Tabs = createVueComponent(
|
|
|
268
279
|
{
|
|
269
280
|
style: {
|
|
270
281
|
display: "flex",
|
|
271
|
-
borderBottom:
|
|
282
|
+
borderBottom: STANDARD_BORDER,
|
|
272
283
|
marginBottom: "8px",
|
|
273
284
|
},
|
|
274
285
|
},
|
|
@@ -321,7 +332,7 @@ const Divider = createVueComponent(DividerApi, ({ props }) => {
|
|
|
321
332
|
const style: Record<string, string> = {
|
|
322
333
|
margin: LEAF_MARGIN,
|
|
323
334
|
border: "none",
|
|
324
|
-
backgroundColor: "#ccc",
|
|
335
|
+
backgroundColor: "var(--a2ui-color-border, #ccc)",
|
|
325
336
|
};
|
|
326
337
|
|
|
327
338
|
if (isVertical) {
|
|
@@ -374,7 +385,7 @@ const Modal = createVueComponent(
|
|
|
374
385
|
"div",
|
|
375
386
|
{
|
|
376
387
|
style: {
|
|
377
|
-
backgroundColor: "#fff",
|
|
388
|
+
backgroundColor: "var(--a2ui-color-surface, #fff)",
|
|
378
389
|
padding: "24px",
|
|
379
390
|
borderRadius: "8px",
|
|
380
391
|
maxWidth: "90%",
|
|
@@ -426,13 +437,13 @@ const Button = createVueComponent(ButtonApi, ({ props, buildChild }) => {
|
|
|
426
437
|
margin: LEAF_MARGIN,
|
|
427
438
|
padding: "8px 16px",
|
|
428
439
|
cursor: "pointer",
|
|
429
|
-
border: props.variant === "borderless" ? "none" :
|
|
440
|
+
border: props.variant === "borderless" ? "none" : STANDARD_BORDER,
|
|
430
441
|
backgroundColor:
|
|
431
442
|
props.variant === "primary"
|
|
432
443
|
? "var(--a2ui-primary-color, #007bff)"
|
|
433
444
|
: props.variant === "borderless"
|
|
434
445
|
? "transparent"
|
|
435
|
-
: "#fff",
|
|
446
|
+
: "var(--a2ui-color-surface, #fff)",
|
|
436
447
|
color: props.variant === "primary" ? "#fff" : "inherit",
|
|
437
448
|
borderRadius: "4px",
|
|
438
449
|
display: "inline-flex",
|
|
@@ -469,7 +480,7 @@ const TextField = createVueComponent(
|
|
|
469
480
|
width: "100%",
|
|
470
481
|
border:
|
|
471
482
|
props.validationErrors && props.validationErrors.length > 0
|
|
472
|
-
? "1px solid red"
|
|
483
|
+
? "1px solid var(--a2ui-color-error, red)"
|
|
473
484
|
: STANDARD_BORDER,
|
|
474
485
|
borderRadius: STANDARD_RADIUS,
|
|
475
486
|
boxSizing: "border-box",
|
|
@@ -519,7 +530,12 @@ const TextField = createVueComponent(
|
|
|
519
530
|
hasError
|
|
520
531
|
? h(
|
|
521
532
|
"span",
|
|
522
|
-
{
|
|
533
|
+
{
|
|
534
|
+
style: {
|
|
535
|
+
fontSize: "12px",
|
|
536
|
+
color: "var(--a2ui-color-error, red)",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
523
539
|
props.validationErrors![0],
|
|
524
540
|
)
|
|
525
541
|
: null,
|
|
@@ -558,7 +574,9 @@ const CheckBox = createVueComponent(
|
|
|
558
574
|
props.setValue((e.target as HTMLInputElement).checked),
|
|
559
575
|
style: {
|
|
560
576
|
cursor: "pointer",
|
|
561
|
-
outline: hasError
|
|
577
|
+
outline: hasError
|
|
578
|
+
? "1px solid var(--a2ui-color-error, red)"
|
|
579
|
+
: "none",
|
|
562
580
|
},
|
|
563
581
|
}),
|
|
564
582
|
props.label
|
|
@@ -568,7 +586,9 @@ const CheckBox = createVueComponent(
|
|
|
568
586
|
for: uniqueId,
|
|
569
587
|
style: {
|
|
570
588
|
cursor: "pointer",
|
|
571
|
-
color: hasError
|
|
589
|
+
color: hasError
|
|
590
|
+
? "var(--a2ui-color-error, red)"
|
|
591
|
+
: "inherit",
|
|
572
592
|
},
|
|
573
593
|
},
|
|
574
594
|
props.label,
|
|
@@ -580,7 +600,11 @@ const CheckBox = createVueComponent(
|
|
|
580
600
|
? h(
|
|
581
601
|
"span",
|
|
582
602
|
{
|
|
583
|
-
style: {
|
|
603
|
+
style: {
|
|
604
|
+
fontSize: "12px",
|
|
605
|
+
color: "var(--a2ui-color-error, red)",
|
|
606
|
+
marginTop: "4px",
|
|
607
|
+
},
|
|
584
608
|
},
|
|
585
609
|
props.validationErrors?.[0],
|
|
586
610
|
)
|
|
@@ -681,7 +705,7 @@ const ChoicePicker = createVueComponent(
|
|
|
681
705
|
: STANDARD_BORDER,
|
|
682
706
|
backgroundColor: isSelected
|
|
683
707
|
? "var(--a2ui-primary-color, #007bff)"
|
|
684
|
-
: "#fff",
|
|
708
|
+
: "var(--a2ui-color-surface, #fff)",
|
|
685
709
|
color: isSelected ? "#fff" : "inherit",
|
|
686
710
|
cursor: "pointer",
|
|
687
711
|
fontSize: "12px",
|
|
@@ -754,7 +778,12 @@ const Slider = createVueComponent(
|
|
|
754
778
|
: null,
|
|
755
779
|
h(
|
|
756
780
|
"span",
|
|
757
|
-
{
|
|
781
|
+
{
|
|
782
|
+
style: {
|
|
783
|
+
fontSize: "12px",
|
|
784
|
+
color: "var(--a2ui-color-muted, #666)",
|
|
785
|
+
},
|
|
786
|
+
},
|
|
758
787
|
String(props.value),
|
|
759
788
|
),
|
|
760
789
|
],
|
|
@@ -12,7 +12,7 @@ export const LEAF_MARGIN = "8px";
|
|
|
12
12
|
export const CONTAINER_PADDING = "16px";
|
|
13
13
|
|
|
14
14
|
/** Standard border for cards and inputs. */
|
|
15
|
-
export const STANDARD_BORDER = "1px solid #ccc";
|
|
15
|
+
export const STANDARD_BORDER = "1px solid var(--a2ui-color-border, #ccc)";
|
|
16
16
|
|
|
17
17
|
/** Standard border radius. */
|
|
18
18
|
export const STANDARD_RADIUS = "8px";
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "@copilotkit/shared";
|
|
8
8
|
import type { Attachment } from "@copilotkit/shared";
|
|
9
9
|
import type { CopilotChatAttachmentQueueProps } from "./types";
|
|
10
|
+
import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
|
|
11
|
+
import { CopilotChatDefaultLabels } from "../../providers/types";
|
|
10
12
|
|
|
11
13
|
const props = withDefaults(defineProps<CopilotChatAttachmentQueueProps>(), {
|
|
12
14
|
attachments: () => [],
|
|
@@ -17,6 +19,9 @@ const emit = defineEmits<{
|
|
|
17
19
|
"remove-attachment": [id: string];
|
|
18
20
|
}>();
|
|
19
21
|
|
|
22
|
+
const config = useCopilotChatConfiguration();
|
|
23
|
+
const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
|
|
24
|
+
|
|
20
25
|
const activeLightboxAttachment = ref<Attachment | null>(null);
|
|
21
26
|
const documentBlobUrl = ref<string | null>(null);
|
|
22
27
|
|
|
@@ -215,7 +220,7 @@ function createBlobUrl(
|
|
|
215
220
|
<button
|
|
216
221
|
type="button"
|
|
217
222
|
class="cpk:absolute cpk:inset-0 cpk:flex cpk:items-center cpk:justify-center cpk:z-10 cpk:cursor-pointer cpk:bg-black/20 cpk:border-none cpk:p-0"
|
|
218
|
-
aria-label="
|
|
223
|
+
:aria-label="labels.attachmentPlayVideoButtonLabel"
|
|
219
224
|
data-testid="copilot-chat-attachment-video-play"
|
|
220
225
|
@click="openLightbox(attachment)"
|
|
221
226
|
>
|
|
@@ -294,7 +299,7 @@ function createBlobUrl(
|
|
|
294
299
|
<button
|
|
295
300
|
type="button"
|
|
296
301
|
class="cpk:absolute cpk:bg-black/60 cpk:text-white cpk:border-none cpk:rounded-full cpk:w-5 cpk:h-5 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:text-[10px] cpk:z-20 cpk:top-1 cpk:right-1"
|
|
297
|
-
aria-label="
|
|
302
|
+
:aria-label="labels.attachmentRemoveButtonLabel"
|
|
298
303
|
@click="emit('remove-attachment', attachment.id)"
|
|
299
304
|
>
|
|
300
305
|
✕
|
|
@@ -312,7 +317,7 @@ function createBlobUrl(
|
|
|
312
317
|
<button
|
|
313
318
|
type="button"
|
|
314
319
|
class="cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none"
|
|
315
|
-
aria-label="
|
|
320
|
+
:aria-label="labels.attachmentPreviewCloseButtonLabel"
|
|
316
321
|
@click.stop="closeLightbox"
|
|
317
322
|
>
|
|
318
323
|
✕
|
|
@@ -1118,7 +1118,7 @@ onBeforeUnmount(() => {
|
|
|
1118
1118
|
ref="slashMenuRef"
|
|
1119
1119
|
data-testid="copilot-slash-menu"
|
|
1120
1120
|
role="listbox"
|
|
1121
|
-
aria-label="
|
|
1121
|
+
:aria-label="labels.chatInputSlashMenuLabel"
|
|
1122
1122
|
class="cpk:absolute cpk:bottom-full cpk:left-0 cpk:right-0 cpk:z-30 cpk:mb-2 cpk:max-h-64 cpk:overflow-y-auto cpk:rounded-lg cpk:border cpk:border-border cpk:bg-white cpk:shadow-lg cpk:dark:border-[#3a3a3a] cpk:dark:bg-[#1f1f1f]"
|
|
1123
1123
|
:style="{ maxHeight: `${5 * 40}px` }"
|
|
1124
1124
|
>
|
|
@@ -1126,7 +1126,7 @@ onBeforeUnmount(() => {
|
|
|
1126
1126
|
v-if="filteredCommands.length === 0"
|
|
1127
1127
|
class="cpk:px-3 cpk:py-2 cpk:text-sm cpk:text-muted-foreground"
|
|
1128
1128
|
>
|
|
1129
|
-
|
|
1129
|
+
{{ labels.chatInputSlashMenuEmptyText }}
|
|
1130
1130
|
</div>
|
|
1131
1131
|
<button
|
|
1132
1132
|
v-for="(command, index) in filteredCommands"
|
|
@@ -3,6 +3,8 @@ import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
|
3
3
|
import type { Message, ReasoningMessage } from "@ag-ui/core";
|
|
4
4
|
import { StreamMarkdown } from "streamdown-vue";
|
|
5
5
|
import { IconChevronRight } from "../icons";
|
|
6
|
+
import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
|
|
7
|
+
import { CopilotChatDefaultLabels } from "../../providers/types";
|
|
6
8
|
|
|
7
9
|
const props = withDefaults(
|
|
8
10
|
defineProps<{
|
|
@@ -26,6 +28,7 @@ defineSlots<{
|
|
|
26
28
|
label: string;
|
|
27
29
|
hasContent: boolean;
|
|
28
30
|
isStreaming: boolean;
|
|
31
|
+
elapsedSeconds: number;
|
|
29
32
|
onClick?: () => void;
|
|
30
33
|
};
|
|
31
34
|
contentView: {
|
|
@@ -47,6 +50,8 @@ defineSlots<{
|
|
|
47
50
|
label: string;
|
|
48
51
|
hasContent: boolean;
|
|
49
52
|
isStreaming: boolean;
|
|
53
|
+
/** Seconds spent reasoning so far, for building a localized label. */
|
|
54
|
+
elapsedSeconds: number;
|
|
50
55
|
onClick?: () => void;
|
|
51
56
|
}) => unknown;
|
|
52
57
|
"content-view"?: (props: {
|
|
@@ -128,9 +133,12 @@ onBeforeUnmount(() => {
|
|
|
128
133
|
clearElapsedInterval();
|
|
129
134
|
});
|
|
130
135
|
|
|
136
|
+
const config = useCopilotChatConfiguration();
|
|
137
|
+
/** `labels.reasoningMessageThinkingLabel` while streaming, then "Thought for …". */
|
|
131
138
|
const label = computed(() =>
|
|
132
139
|
isStreaming.value
|
|
133
|
-
?
|
|
140
|
+
? (config.value?.labels ?? CopilotChatDefaultLabels)
|
|
141
|
+
.reasoningMessageThinkingLabel
|
|
134
142
|
: `Thought for ${formatDuration(elapsed.value)}`,
|
|
135
143
|
);
|
|
136
144
|
|
|
@@ -153,6 +161,7 @@ function toggleOpen() {
|
|
|
153
161
|
label,
|
|
154
162
|
hasContent,
|
|
155
163
|
isStreaming,
|
|
164
|
+
elapsedSeconds: elapsed,
|
|
156
165
|
onClick: hasContent ? toggleOpen : undefined,
|
|
157
166
|
}"
|
|
158
167
|
:content-view="{
|
|
@@ -175,6 +184,7 @@ function toggleOpen() {
|
|
|
175
184
|
:label="label"
|
|
176
185
|
:has-content="hasContent"
|
|
177
186
|
:is-streaming="isStreaming"
|
|
187
|
+
:elapsed-seconds="elapsed"
|
|
178
188
|
:on-click="hasContent ? toggleOpen : undefined"
|
|
179
189
|
>
|
|
180
190
|
<button
|
|
@@ -107,7 +107,10 @@ function toggleDrawer() {
|
|
|
107
107
|
data-testid="drawer-launcher"
|
|
108
108
|
class="cpk:inline-flex cpk:size-8 cpk:items-center cpk:justify-center cpk:rounded-full cpk:text-muted-foreground cpk:transition cpk:cursor-pointer cpk:hover:bg-muted cpk:hover:text-foreground cpk:focus-visible:outline-none cpk:focus-visible:ring-2 cpk:focus-visible:ring-ring"
|
|
109
109
|
:aria-expanded="config?.drawerOpen ?? false"
|
|
110
|
-
aria-label="
|
|
110
|
+
:aria-label="
|
|
111
|
+
config?.labels.modalHeaderThreadsButtonLabel ??
|
|
112
|
+
CopilotChatDefaultLabels.modalHeaderThreadsButtonLabel
|
|
113
|
+
"
|
|
111
114
|
@click="toggleDrawer"
|
|
112
115
|
>
|
|
113
116
|
<IconPanelLeftOpen class="cpk:h-4 cpk:w-4" aria-hidden="true" />
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { computed, defineComponent, h, useAttrs } from "vue";
|
|
2
2
|
import { IconX } from "../icons";
|
|
3
|
+
import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
|
|
4
|
+
import { CopilotChatDefaultLabels } from "../../providers/types";
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Closes the popup or sidebar. Its accessible name is the `aria-label` the
|
|
8
|
+
* parent passes, or `labels.modalHeaderCloseButtonLabel`.
|
|
9
|
+
*/
|
|
4
10
|
export default defineComponent({
|
|
5
11
|
name: "CopilotModalHeaderCloseButton",
|
|
6
12
|
inheritAttrs: false,
|
|
7
13
|
setup(_, { slots }) {
|
|
8
14
|
const attrs = useAttrs();
|
|
15
|
+
const config = useCopilotChatConfiguration();
|
|
9
16
|
const ariaLabel = computed(() =>
|
|
10
|
-
typeof attrs["aria-label"] === "string"
|
|
17
|
+
typeof attrs["aria-label"] === "string"
|
|
18
|
+
? attrs["aria-label"]
|
|
19
|
+
: (config.value?.labels ?? CopilotChatDefaultLabels)
|
|
20
|
+
.modalHeaderCloseButtonLabel,
|
|
11
21
|
);
|
|
12
22
|
|
|
13
23
|
return () => {
|
|
@@ -277,7 +277,9 @@ onBeforeUnmount(() => {
|
|
|
277
277
|
data-copilot-sidebar
|
|
278
278
|
:class="asideClass"
|
|
279
279
|
:style="asideStyle"
|
|
280
|
-
aria-label="
|
|
280
|
+
:aria-label="
|
|
281
|
+
config?.labels.sidebarLabel ?? CopilotChatDefaultLabels.sidebarLabel
|
|
282
|
+
"
|
|
281
283
|
:aria-hidden="!isSidebarOpen"
|
|
282
284
|
role="complementary"
|
|
283
285
|
v-bind="asideAttrs"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { defineComponent, nextTick, onMounted } from "vue";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/vue";
|
|
4
|
+
import type { Message, ReasoningMessage } from "@ag-ui/core";
|
|
5
|
+
import type { Attachment } from "@copilotkit/shared";
|
|
6
|
+
import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
|
|
7
|
+
import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
|
|
8
|
+
import { useCopilotChatConfiguration } from "../../../providers/useCopilotChatConfiguration";
|
|
9
|
+
import type { CopilotChatLabels } from "../../../providers/types";
|
|
10
|
+
import CopilotChatReasoningMessage from "../CopilotChatReasoningMessage.vue";
|
|
11
|
+
import CopilotChatAttachmentQueue from "../CopilotChatAttachmentQueue.vue";
|
|
12
|
+
import CopilotChatInput from "../CopilotChatInput.vue";
|
|
13
|
+
import { CopilotModalHeader, CopilotSidebarView } from "../index";
|
|
14
|
+
|
|
15
|
+
// Strings an app translates through `labels`. Every one of them used to be
|
|
16
|
+
// hard-coded English.
|
|
17
|
+
const germanLabels: Partial<Record<keyof CopilotChatLabels, string>> = {
|
|
18
|
+
reasoningMessageThinkingLabel: "Denkt nach…",
|
|
19
|
+
chatInputSlashMenuLabel: "Befehle",
|
|
20
|
+
chatInputSlashMenuEmptyText: "Keine Befehle gefunden",
|
|
21
|
+
attachmentRemoveButtonLabel: "Anhang entfernen",
|
|
22
|
+
attachmentPlayVideoButtonLabel: "Video abspielen",
|
|
23
|
+
attachmentPreviewCloseButtonLabel: "Vorschau schließen",
|
|
24
|
+
modalHeaderCloseButtonLabel: "Schließen",
|
|
25
|
+
modalHeaderThreadsButtonLabel: "Unterhaltungen öffnen",
|
|
26
|
+
sidebarLabel: "Chat-Seitenleiste",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const reasoning = {
|
|
30
|
+
id: "reasoning-1",
|
|
31
|
+
role: "reasoning",
|
|
32
|
+
content: "",
|
|
33
|
+
} as ReasoningMessage;
|
|
34
|
+
|
|
35
|
+
const video: Attachment = {
|
|
36
|
+
id: "clip",
|
|
37
|
+
type: "video",
|
|
38
|
+
source: { type: "data", value: "AAAA", mimeType: "video/mp4" },
|
|
39
|
+
filename: "clip.mp4",
|
|
40
|
+
status: "ready",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Registers a threads drawer, which the header needs before it shows its launcher. */
|
|
44
|
+
const DrawerRegistrar = defineComponent({
|
|
45
|
+
setup() {
|
|
46
|
+
const config = useCopilotChatConfiguration();
|
|
47
|
+
onMounted(() => config.value?.registerDrawer());
|
|
48
|
+
},
|
|
49
|
+
template: `<span />`,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Renders `template` inside the providers, with the German labels configured. */
|
|
53
|
+
function renderWithLabels(template: string) {
|
|
54
|
+
const Host = defineComponent({
|
|
55
|
+
components: {
|
|
56
|
+
CopilotKitProvider,
|
|
57
|
+
CopilotChatConfigurationProvider,
|
|
58
|
+
CopilotChatReasoningMessage,
|
|
59
|
+
CopilotChatAttachmentQueue,
|
|
60
|
+
CopilotChatInput,
|
|
61
|
+
CopilotModalHeader,
|
|
62
|
+
CopilotSidebarView,
|
|
63
|
+
DrawerRegistrar,
|
|
64
|
+
},
|
|
65
|
+
setup() {
|
|
66
|
+
return {
|
|
67
|
+
labels: germanLabels,
|
|
68
|
+
reasoning,
|
|
69
|
+
reasoningMessages: [reasoning] as Message[],
|
|
70
|
+
attachments: [video],
|
|
71
|
+
toolsMenu: [{ label: "Say hi", action: vi.fn() }],
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
template: `
|
|
75
|
+
<CopilotKitProvider runtime-url="/api/copilotkit">
|
|
76
|
+
<CopilotChatConfigurationProvider thread-id="thread-1" :labels="labels">
|
|
77
|
+
${template}
|
|
78
|
+
</CopilotChatConfigurationProvider>
|
|
79
|
+
</CopilotKitProvider>
|
|
80
|
+
`,
|
|
81
|
+
});
|
|
82
|
+
return render(Host);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("chat strings from labels", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.stubGlobal(
|
|
88
|
+
"ResizeObserver",
|
|
89
|
+
class {
|
|
90
|
+
observe() {}
|
|
91
|
+
unobserve() {}
|
|
92
|
+
disconnect() {}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
vi.useRealTimers();
|
|
99
|
+
vi.unstubAllGlobals();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("uses the configured reasoning label while the model reasons", () => {
|
|
103
|
+
renderWithLabels(`
|
|
104
|
+
<CopilotChatReasoningMessage
|
|
105
|
+
:message="reasoning"
|
|
106
|
+
:messages="reasoningMessages"
|
|
107
|
+
:is-running="true"
|
|
108
|
+
/>
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText("Denkt nach…")).toBeDefined();
|
|
112
|
+
expect(screen.queryByText("Thinking…")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("passes the elapsed reasoning time to a custom header", async () => {
|
|
116
|
+
vi.useFakeTimers();
|
|
117
|
+
renderWithLabels(`
|
|
118
|
+
<CopilotChatReasoningMessage
|
|
119
|
+
:message="reasoning"
|
|
120
|
+
:messages="reasoningMessages"
|
|
121
|
+
:is-running="true"
|
|
122
|
+
>
|
|
123
|
+
<template #header="{ elapsedSeconds }">
|
|
124
|
+
<span>{{ Math.round(elapsedSeconds) }} Sekunden</span>
|
|
125
|
+
</template>
|
|
126
|
+
</CopilotChatReasoningMessage>
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
expect(screen.getByText("0 Sekunden")).toBeDefined();
|
|
130
|
+
vi.advanceTimersByTime(3000);
|
|
131
|
+
await nextTick();
|
|
132
|
+
expect(screen.getByText("3 Sekunden")).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("names the slash-command menu and its empty state", async () => {
|
|
136
|
+
renderWithLabels(`<CopilotChatInput :tools-menu="toolsMenu" />`);
|
|
137
|
+
|
|
138
|
+
await fireEvent.input(screen.getByRole("textbox"), {
|
|
139
|
+
target: { value: "/zzz" },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(
|
|
143
|
+
await screen.findByRole("listbox", { name: "Befehle" }),
|
|
144
|
+
).toBeDefined();
|
|
145
|
+
expect(screen.getByText("Keine Befehle gefunden")).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("names the attachment queue buttons and the preview close button", async () => {
|
|
149
|
+
renderWithLabels(
|
|
150
|
+
`<CopilotChatAttachmentQueue :attachments="attachments" />`,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
screen.getByRole("button", { name: "Anhang entfernen" }),
|
|
155
|
+
).toBeDefined();
|
|
156
|
+
await fireEvent.click(
|
|
157
|
+
screen.getByRole("button", { name: "Video abspielen" }),
|
|
158
|
+
);
|
|
159
|
+
expect(
|
|
160
|
+
await screen.findByRole("button", { name: "Vorschau schließen" }),
|
|
161
|
+
).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("names the modal header close and threads buttons", async () => {
|
|
165
|
+
// A mobile viewport: the threads launcher only renders there.
|
|
166
|
+
vi.stubGlobal(
|
|
167
|
+
"matchMedia",
|
|
168
|
+
vi.fn().mockReturnValue({
|
|
169
|
+
matches: true,
|
|
170
|
+
media: "(max-width: 767px)",
|
|
171
|
+
onchange: null,
|
|
172
|
+
addEventListener: vi.fn(),
|
|
173
|
+
removeEventListener: vi.fn(),
|
|
174
|
+
addListener: vi.fn(),
|
|
175
|
+
removeListener: vi.fn(),
|
|
176
|
+
dispatchEvent: vi.fn(),
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
renderWithLabels(`<CopilotModalHeader /><DrawerRegistrar />`);
|
|
180
|
+
|
|
181
|
+
expect(
|
|
182
|
+
await screen.findByRole("button", { name: "Unterhaltungen öffnen" }),
|
|
183
|
+
).toBeDefined();
|
|
184
|
+
expect(screen.getByRole("button", { name: "Schließen" })).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("names the sidebar", () => {
|
|
188
|
+
renderWithLabels(`<CopilotSidebarView />`);
|
|
189
|
+
|
|
190
|
+
// The sidebar starts closed, so it is aria-hidden until opened.
|
|
191
|
+
expect(
|
|
192
|
+
screen.getByRole("complementary", {
|
|
193
|
+
name: "Chat-Seitenleiste",
|
|
194
|
+
hidden: true,
|
|
195
|
+
}),
|
|
196
|
+
).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -90,6 +90,21 @@ export interface CopilotKitProviderProps {
|
|
|
90
90
|
theme?: A2UITheme;
|
|
91
91
|
catalog?: any;
|
|
92
92
|
loadingComponent?: Component;
|
|
93
|
+
/**
|
|
94
|
+
* When true (the default), the full component schemas from the catalog are
|
|
95
|
+
* sent as agent context so the agent knows what components and props are
|
|
96
|
+
* available. The A2UI middleware can overwrite this with a server-side
|
|
97
|
+
* schema if configured.
|
|
98
|
+
*
|
|
99
|
+
* When false, the agent context still describes the catalog, but only by its
|
|
100
|
+
* catalog ID and the names of its custom components, without their JSON
|
|
101
|
+
* schemas. The component schema entry and the A2UI generation and design
|
|
102
|
+
* guidelines are not sent. With the CopilotKit LangGraph middleware's
|
|
103
|
+
* injected A2UI tool, the A2UI subagent then gets the component names only;
|
|
104
|
+
* a host can still give it a composition guide through the middleware's
|
|
105
|
+
* guidelines (`a2uiParams.guidelines.compositionGuide` in TypeScript,
|
|
106
|
+
* `a2ui_params["guidelines"]["composition_guide"]` in Python).
|
|
107
|
+
*/
|
|
93
108
|
includeSchema?: boolean;
|
|
94
109
|
};
|
|
95
110
|
/** Configuration for the built-in MCP Apps renderer. */
|