@copilotkit/shared 1.55.0-next.9 → 1.55.1-next.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/CHANGELOG.md +13 -1
- package/dist/a2ui-prompts.cjs +106 -0
- package/dist/a2ui-prompts.cjs.map +1 -0
- package/dist/a2ui-prompts.d.cts +20 -0
- package/dist/a2ui-prompts.d.cts.map +1 -0
- package/dist/a2ui-prompts.d.mts +20 -0
- package/dist/a2ui-prompts.d.mts.map +1 -0
- package/dist/a2ui-prompts.mjs +104 -0
- package/dist/a2ui-prompts.mjs.map +1 -0
- package/dist/attachments/types.d.cts +54 -0
- package/dist/attachments/types.d.cts.map +1 -0
- package/dist/attachments/types.d.mts +54 -0
- package/dist/attachments/types.d.mts.map +1 -0
- package/dist/attachments/utils.cjs +134 -0
- package/dist/attachments/utils.cjs.map +1 -0
- package/dist/attachments/utils.d.cts +42 -0
- package/dist/attachments/utils.d.cts.map +1 -0
- package/dist/attachments/utils.d.mts +42 -0
- package/dist/attachments/utils.d.mts.map +1 -0
- package/dist/attachments/utils.mjs +126 -0
- package/dist/attachments/utils.mjs.map +1 -0
- package/dist/index.cjs +29 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +20 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +19 -3
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +259 -9
- package/dist/index.umd.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/types/message.d.cts +15 -4
- package/dist/types/message.d.cts.map +1 -1
- package/dist/types/message.d.mts +15 -4
- package/dist/types/message.d.mts.map +1 -1
- package/dist/utils/types.cjs.map +1 -1
- package/dist/utils/types.d.cts +1 -0
- package/dist/utils/types.d.cts.map +1 -1
- package/dist/utils/types.d.mts +1 -0
- package/dist/utils/types.d.mts.map +1 -1
- package/dist/utils/types.mjs.map +1 -1
- package/package.json +33 -34
- package/src/a2ui-prompts.ts +101 -0
- package/src/attachments/__tests__/utils.test.ts +198 -0
- package/src/attachments/index.ts +19 -0
- package/src/attachments/types.ts +67 -0
- package/src/attachments/utils.ts +164 -0
- package/src/index.ts +43 -1
- package/src/types/__tests__/message.test.ts +62 -0
- package/src/types/message.ts +27 -3
- package/src/utils/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/shared",
|
|
3
|
+
"version": "1.55.1-next.0",
|
|
3
4
|
"private": false,
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"assistant",
|
|
8
|
+
"automation",
|
|
9
|
+
"copilot",
|
|
10
|
+
"copilotkit",
|
|
11
|
+
"javascript",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"react",
|
|
15
|
+
"textarea"
|
|
16
|
+
],
|
|
4
17
|
"homepage": "https://github.com/CopilotKit/CopilotKit",
|
|
18
|
+
"license": "MIT",
|
|
5
19
|
"repository": {
|
|
6
20
|
"type": "git",
|
|
7
21
|
"url": "https://github.com/CopilotKit/CopilotKit.git"
|
|
8
22
|
},
|
|
9
|
-
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
11
|
-
},
|
|
12
23
|
"type": "module",
|
|
13
|
-
"version": "1.55.0-next.9",
|
|
14
24
|
"sideEffects": false,
|
|
15
25
|
"main": "./dist/index.cjs",
|
|
16
26
|
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"unpkg": "./dist/index.umd.js",
|
|
29
|
+
"jsdelivr": "./dist/index.umd.js",
|
|
17
30
|
"exports": {
|
|
18
31
|
".": {
|
|
19
32
|
"import": "./dist/index.mjs",
|
|
@@ -21,10 +34,21 @@
|
|
|
21
34
|
},
|
|
22
35
|
"./package.json": "./package.json"
|
|
23
36
|
},
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@ag-ui/client": "0.0.52",
|
|
42
|
+
"@copilotkit/license-verifier": "0.0.1-a1",
|
|
43
|
+
"@segment/analytics-node": "^2.1.2",
|
|
44
|
+
"@standard-schema/spec": "^1.0.0",
|
|
45
|
+
"chalk": "4.1.2",
|
|
46
|
+
"graphql": "^16.8.1",
|
|
47
|
+
"partial-json": "^0.1.7",
|
|
48
|
+
"uuid": "^11.1.0",
|
|
49
|
+
"zod": "^3.23.3",
|
|
50
|
+
"zod-to-json-schema": "^3.23.5"
|
|
51
|
+
},
|
|
28
52
|
"devDependencies": {
|
|
29
53
|
"@types/uuid": "^10.0.0",
|
|
30
54
|
"@valibot/to-json-schema": "^1.5.0",
|
|
@@ -34,36 +58,11 @@
|
|
|
34
58
|
"typescript": "^5.2.3",
|
|
35
59
|
"valibot": "^1.2.0",
|
|
36
60
|
"vitest": "^3.2.4",
|
|
37
|
-
"zod-to-json-schema": "^3.23.5",
|
|
38
|
-
"eslint-config-custom": "1.4.12",
|
|
39
61
|
"tsconfig": "1.4.12"
|
|
40
62
|
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@ag-ui/client": "0.0.47",
|
|
43
|
-
"@segment/analytics-node": "^2.1.2",
|
|
44
|
-
"@standard-schema/spec": "^1.0.0",
|
|
45
|
-
"chalk": "4.1.2",
|
|
46
|
-
"graphql": "^16.8.1",
|
|
47
|
-
"partial-json": "^0.1.7",
|
|
48
|
-
"uuid": "^11.1.0",
|
|
49
|
-
"@copilotkit/license-verifier": "0.0.1-a1",
|
|
50
|
-
"zod": "^3.23.3"
|
|
51
|
-
},
|
|
52
63
|
"peerDependencies": {
|
|
53
|
-
"@ag-ui/core": "
|
|
64
|
+
"@ag-ui/core": ">=0.0.48"
|
|
54
65
|
},
|
|
55
|
-
"keywords": [
|
|
56
|
-
"copilotkit",
|
|
57
|
-
"copilot",
|
|
58
|
-
"react",
|
|
59
|
-
"nextjs",
|
|
60
|
-
"nodejs",
|
|
61
|
-
"ai",
|
|
62
|
-
"assistant",
|
|
63
|
-
"javascript",
|
|
64
|
-
"automation",
|
|
65
|
-
"textarea"
|
|
66
|
-
],
|
|
67
66
|
"scripts": {
|
|
68
67
|
"build": "tsdown",
|
|
69
68
|
"dev": "tsdown --watch",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default A2UI generation and design guideline prompts.
|
|
3
|
+
*
|
|
4
|
+
* These are the canonical prompt fragments that instruct an LLM how to call
|
|
5
|
+
* the render_a2ui tool, how to bind data, and how to style surfaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generation guidelines — protocol rules, tool arguments, path rules,
|
|
10
|
+
* data model format, and form/two-way-binding instructions.
|
|
11
|
+
*/
|
|
12
|
+
export const A2UI_DEFAULT_GENERATION_GUIDELINES = `\
|
|
13
|
+
Generate A2UI v0.9 JSON.
|
|
14
|
+
|
|
15
|
+
## A2UI Protocol Instructions
|
|
16
|
+
|
|
17
|
+
A2UI (Agent to UI) is a protocol for rendering rich UI surfaces from agent responses.
|
|
18
|
+
|
|
19
|
+
CRITICAL: You MUST call the render_a2ui tool with ALL of these arguments:
|
|
20
|
+
- surfaceId: A unique ID for the surface (e.g. "product-comparison")
|
|
21
|
+
- components: REQUIRED — the A2UI component array. NEVER omit this. Only use
|
|
22
|
+
components listed in the Available Components schema provided as context.
|
|
23
|
+
- data: OPTIONAL — a JSON object written to the root of the surface data model.
|
|
24
|
+
Use for pre-filling form values or providing data for path-bound components.
|
|
25
|
+
- every component must have the "component" field specifying the component type.
|
|
26
|
+
ONLY use component names from the Available Components schema — do NOT invent
|
|
27
|
+
component names or use names not in the schema.
|
|
28
|
+
|
|
29
|
+
COMPONENT ID RULES:
|
|
30
|
+
- Every component ID must be unique within the surface.
|
|
31
|
+
- A component MUST NOT reference itself as child/children. This causes a
|
|
32
|
+
circular dependency error. For example, if a component has id="avatar",
|
|
33
|
+
its child must be a DIFFERENT id (e.g. "avatar-img"), never "avatar".
|
|
34
|
+
- The child/children tree must be a DAG — no cycles allowed.
|
|
35
|
+
|
|
36
|
+
REPEATING CONTENT (TEMPLATES):
|
|
37
|
+
To repeat a component for each item in an array, use the structural children format:
|
|
38
|
+
children: { componentId: "card-id", path: "/items" }
|
|
39
|
+
This tells the renderer to create one instance of "card-id" per item in the "/items" array.
|
|
40
|
+
|
|
41
|
+
PATH RULES FOR TEMPLATES:
|
|
42
|
+
Components inside a repeating template use RELATIVE paths (no leading slash).
|
|
43
|
+
The path is resolved relative to each array item automatically.
|
|
44
|
+
If a container has children: { componentId: "card", path: "/items" } and each item
|
|
45
|
+
has a key "name", use { "path": "name" } (NO leading slash — relative to item).
|
|
46
|
+
CRITICAL: Do NOT use "/name" (absolute) inside templates — use "name" (relative).
|
|
47
|
+
The container's path ("/items") uses a leading slash (absolute), but all
|
|
48
|
+
components INSIDE the template use paths WITHOUT leading slash.
|
|
49
|
+
|
|
50
|
+
DATA MODEL:
|
|
51
|
+
The "data" key in the tool args is a plain JSON object that initializes the surface
|
|
52
|
+
data model. Components bound to paths (e.g. "value": { "path": "/form/name" })
|
|
53
|
+
read from and write to this data model. Examples:
|
|
54
|
+
For forms: "data": { "form": { "name": "Alice", "email": "" } }
|
|
55
|
+
For lists: "data": { "items": [{"name": "Product A"}, {"name": "Product B"}] }
|
|
56
|
+
For mixed: "data": { "form": { "query": "" }, "results": [...] }
|
|
57
|
+
|
|
58
|
+
FORMS AND TWO-WAY DATA BINDING:
|
|
59
|
+
To create editable forms, bind input components to data model paths using { "path": "..." }.
|
|
60
|
+
The client automatically writes user input back to the data model at the bound path.
|
|
61
|
+
CRITICAL: Using a literal value (e.g. "value": "") makes the field READ-ONLY.
|
|
62
|
+
You MUST use { "path": "..." } to make inputs editable.
|
|
63
|
+
|
|
64
|
+
Input components use "value" as the binding property:
|
|
65
|
+
"value": { "path": "/form/fieldName" }
|
|
66
|
+
|
|
67
|
+
To retrieve form values when a button is clicked, include "context" with path references
|
|
68
|
+
in the button's action. Paths are resolved to their current values at click time:
|
|
69
|
+
"action": { "event": { "name": "submit", "context": { "userName": { "path": "/form/name" } } } }
|
|
70
|
+
|
|
71
|
+
To pre-fill form values, pass initial data via the "data" tool argument:
|
|
72
|
+
"data": { "form": { "name": "Markus" } }`;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Design guidelines — visual design rules, component hierarchy tips,
|
|
76
|
+
* and action handler patterns.
|
|
77
|
+
*/
|
|
78
|
+
export const A2UI_DEFAULT_DESIGN_GUIDELINES = `\
|
|
79
|
+
Create polished, visually appealing interfaces. ONLY use components listed in the
|
|
80
|
+
Available Components schema — do NOT use component names that are not in the schema.
|
|
81
|
+
|
|
82
|
+
Design principles:
|
|
83
|
+
- Create clear visual hierarchy within cards and layouts.
|
|
84
|
+
- Keep cards clean — avoid clutter. Whitespace is good.
|
|
85
|
+
- Use consistent surfaceIds (lowercase, hyphenated).
|
|
86
|
+
- NEVER use the same ID for a component and its child — this creates a
|
|
87
|
+
circular dependency. E.g. if id="avatar", child must NOT be "avatar".
|
|
88
|
+
- For side-by-side comparisons, use a container with structural children
|
|
89
|
+
(children: { componentId, path }) to repeat a card template per data item.
|
|
90
|
+
- Include images when relevant (logos, icons, product photos):
|
|
91
|
+
- Prefer company logos via Google favicons: https://www.google.com/s2/favicons?domain=example.com&sz=128
|
|
92
|
+
- Do NOT invent Unsplash photo-IDs — they will 404. Only use real, known URLs.
|
|
93
|
+
- For buttons: action MUST use this exact nested format:
|
|
94
|
+
"action": { "event": { "name": "myAction", "context": { "key": "value" } } }
|
|
95
|
+
The "event" key holds an OBJECT with "name" (required) and "context" (optional).
|
|
96
|
+
Do NOT use a flat format like {"event": "name"} — "event" must be an object.
|
|
97
|
+
- For forms: every input MUST use path binding on the "value" property
|
|
98
|
+
(e.g. "value": { "path": "/form/name" }) to be editable. The submit button's
|
|
99
|
+
action context MUST reference the same paths to capture the user's input.
|
|
100
|
+
|
|
101
|
+
Use the SAME surfaceId as the main surface. Match action names to button action event names.`;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getModalityFromMimeType,
|
|
3
|
+
formatFileSize,
|
|
4
|
+
exceedsMaxSize,
|
|
5
|
+
matchesAcceptFilter,
|
|
6
|
+
} from "../utils";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
function mockFile(size: number): File {
|
|
13
|
+
return { size } as File;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mockFileWithType(type: string, name = "test"): File {
|
|
17
|
+
return { type, name } as File;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// getModalityFromMimeType
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
describe("getModalityFromMimeType", () => {
|
|
25
|
+
it('returns "image" for image/png', () => {
|
|
26
|
+
expect(getModalityFromMimeType("image/png")).toBe("image");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns "image" for image/jpeg', () => {
|
|
30
|
+
expect(getModalityFromMimeType("image/jpeg")).toBe("image");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns "audio" for audio/mp3', () => {
|
|
34
|
+
expect(getModalityFromMimeType("audio/mp3")).toBe("audio");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns "audio" for audio/wav', () => {
|
|
38
|
+
expect(getModalityFromMimeType("audio/wav")).toBe("audio");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns "video" for video/mp4', () => {
|
|
42
|
+
expect(getModalityFromMimeType("video/mp4")).toBe("video");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns "video" for video/webm', () => {
|
|
46
|
+
expect(getModalityFromMimeType("video/webm")).toBe("video");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns "document" for application/pdf', () => {
|
|
50
|
+
expect(getModalityFromMimeType("application/pdf")).toBe("document");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns "document" for text/plain', () => {
|
|
54
|
+
expect(getModalityFromMimeType("text/plain")).toBe("document");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns "document" for empty string (fallback)', () => {
|
|
58
|
+
expect(getModalityFromMimeType("")).toBe("document");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// formatFileSize
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe("formatFileSize", () => {
|
|
67
|
+
it('formats 0 bytes as "0 B"', () => {
|
|
68
|
+
expect(formatFileSize(0)).toBe("0 B");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('formats 512 bytes as "512 B"', () => {
|
|
72
|
+
expect(formatFileSize(512)).toBe("512 B");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('formats 1023 bytes as "1023 B"', () => {
|
|
76
|
+
expect(formatFileSize(1023)).toBe("1023 B");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('formats 1024 bytes as "1.0 KB"', () => {
|
|
80
|
+
expect(formatFileSize(1024)).toBe("1.0 KB");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('formats 1536 bytes as "1.5 KB"', () => {
|
|
84
|
+
expect(formatFileSize(1536)).toBe("1.5 KB");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('formats 1048575 bytes as "1024.0 KB" (one byte under 1 MB)', () => {
|
|
88
|
+
expect(formatFileSize(1048575)).toBe("1024.0 KB");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('formats 1048576 bytes as "1.0 MB"', () => {
|
|
92
|
+
expect(formatFileSize(1048576)).toBe("1.0 MB");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('formats 10485760 bytes as "10.0 MB"', () => {
|
|
96
|
+
expect(formatFileSize(10485760)).toBe("10.0 MB");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// exceedsMaxSize
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
const MB_20 = 20 * 1024 * 1024;
|
|
105
|
+
|
|
106
|
+
describe("exceedsMaxSize", () => {
|
|
107
|
+
it("returns false for a file exactly at the 20 MB default limit", () => {
|
|
108
|
+
expect(exceedsMaxSize(mockFile(MB_20))).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns true for a file one byte over the 20 MB default limit", () => {
|
|
112
|
+
expect(exceedsMaxSize(mockFile(MB_20 + 1))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns false for a file well under the default limit", () => {
|
|
116
|
+
expect(exceedsMaxSize(mockFile(1024))).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns true when file exceeds a custom maxSize", () => {
|
|
120
|
+
const MB_5 = 5 * 1024 * 1024;
|
|
121
|
+
const MB_10 = 10 * 1024 * 1024;
|
|
122
|
+
expect(exceedsMaxSize(mockFile(MB_10), MB_5)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// matchesAcceptFilter
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe("matchesAcceptFilter", () => {
|
|
131
|
+
it('returns true for accept "*/*" regardless of file type', () => {
|
|
132
|
+
expect(matchesAcceptFilter(mockFileWithType("image/png"), "*/*")).toBe(
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns true for empty accept string (accept all)", () => {
|
|
138
|
+
expect(matchesAcceptFilter(mockFileWithType("image/png"), "")).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns true when accept "image/*" matches "image/png"', () => {
|
|
142
|
+
expect(matchesAcceptFilter(mockFileWithType("image/png"), "image/*")).toBe(
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns false when accept "image/*" rejects "audio/mp3"', () => {
|
|
148
|
+
expect(matchesAcceptFilter(mockFileWithType("audio/mp3"), "image/*")).toBe(
|
|
149
|
+
false,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns true for exact match accept "application/pdf"', () => {
|
|
154
|
+
expect(
|
|
155
|
+
matchesAcceptFilter(
|
|
156
|
+
mockFileWithType("application/pdf"),
|
|
157
|
+
"application/pdf",
|
|
158
|
+
),
|
|
159
|
+
).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns false when exact accept "application/pdf" rejects "image/png"', () => {
|
|
163
|
+
expect(
|
|
164
|
+
matchesAcceptFilter(mockFileWithType("image/png"), "application/pdf"),
|
|
165
|
+
).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns true for comma-separated accept "image/*,application/pdf" with "image/jpeg"', () => {
|
|
169
|
+
expect(
|
|
170
|
+
matchesAcceptFilter(
|
|
171
|
+
mockFileWithType("image/jpeg"),
|
|
172
|
+
"image/*,application/pdf",
|
|
173
|
+
),
|
|
174
|
+
).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns true for comma-separated accept "image/*,application/pdf" with "application/pdf"', () => {
|
|
178
|
+
expect(
|
|
179
|
+
matchesAcceptFilter(
|
|
180
|
+
mockFileWithType("application/pdf"),
|
|
181
|
+
"image/*,application/pdf",
|
|
182
|
+
),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("handles whitespace in comma-separated accept values", () => {
|
|
187
|
+
expect(
|
|
188
|
+
matchesAcceptFilter(
|
|
189
|
+
mockFileWithType("application/pdf"),
|
|
190
|
+
"image/* , application/pdf",
|
|
191
|
+
),
|
|
192
|
+
).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("returns false for empty file type against a specific filter", () => {
|
|
196
|
+
expect(matchesAcceptFilter(mockFileWithType(""), "image/*")).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AttachmentsConfig,
|
|
3
|
+
AttachmentUploadResult,
|
|
4
|
+
AttachmentUploadError,
|
|
5
|
+
AttachmentUploadErrorReason,
|
|
6
|
+
Attachment,
|
|
7
|
+
AttachmentModality,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
getModalityFromMimeType,
|
|
12
|
+
formatFileSize,
|
|
13
|
+
exceedsMaxSize,
|
|
14
|
+
readFileAsBase64,
|
|
15
|
+
generateVideoThumbnail,
|
|
16
|
+
matchesAcceptFilter,
|
|
17
|
+
getSourceUrl,
|
|
18
|
+
getDocumentIcon,
|
|
19
|
+
} from "./utils";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InputContentDataSource,
|
|
3
|
+
InputContentUrlSource,
|
|
4
|
+
} from "@ag-ui/core";
|
|
5
|
+
|
|
6
|
+
export interface AttachmentUploadDataResult {
|
|
7
|
+
type: "data";
|
|
8
|
+
value: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
/** Custom metadata to include in the InputContent part (merged with auto-generated metadata like filename). */
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AttachmentUploadUrlResult {
|
|
15
|
+
type: "url";
|
|
16
|
+
value: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
/** Custom metadata to include in the InputContent part (merged with auto-generated metadata like filename). */
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AttachmentUploadResult =
|
|
23
|
+
| AttachmentUploadDataResult
|
|
24
|
+
| AttachmentUploadUrlResult;
|
|
25
|
+
|
|
26
|
+
export type AttachmentUploadErrorReason =
|
|
27
|
+
| "file-too-large"
|
|
28
|
+
| "invalid-type"
|
|
29
|
+
| "upload-failed";
|
|
30
|
+
|
|
31
|
+
export interface AttachmentUploadError {
|
|
32
|
+
/** Why the upload failed. */
|
|
33
|
+
reason: AttachmentUploadErrorReason;
|
|
34
|
+
/** The file that failed to upload. */
|
|
35
|
+
file: File;
|
|
36
|
+
/** Human-readable error message. */
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AttachmentsConfig {
|
|
41
|
+
/** Enable file attachments in the chat input */
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
/** MIME type filter for the file input, default all files */
|
|
44
|
+
accept?: string;
|
|
45
|
+
/** Maximum file size in bytes, default 20MB (20 * 1024 * 1024) */
|
|
46
|
+
maxSize?: number;
|
|
47
|
+
/** Custom upload handler. Return an InputContentSource with optional metadata. */
|
|
48
|
+
onUpload?: (
|
|
49
|
+
file: File,
|
|
50
|
+
) => AttachmentUploadResult | Promise<AttachmentUploadResult>;
|
|
51
|
+
/** Called when an attachment fails validation or upload. Use this to show a toast or inline error. */
|
|
52
|
+
onUploadFailed?: (error: AttachmentUploadError) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type AttachmentModality = "image" | "audio" | "video" | "document";
|
|
56
|
+
|
|
57
|
+
export interface Attachment {
|
|
58
|
+
id: string;
|
|
59
|
+
type: AttachmentModality;
|
|
60
|
+
source: InputContentDataSource | InputContentUrlSource;
|
|
61
|
+
filename?: string;
|
|
62
|
+
size?: number;
|
|
63
|
+
status: "uploading" | "ready";
|
|
64
|
+
thumbnail?: string;
|
|
65
|
+
/** Custom metadata from onUpload, included in the InputContent part. */
|
|
66
|
+
metadata?: Record<string, unknown>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { AttachmentModality } from "./types";
|
|
2
|
+
import type { InputContentSource } from "../types/message";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_SIZE = 20 * 1024 * 1024; // 20MB
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Derive the attachment modality from a MIME type string.
|
|
8
|
+
*/
|
|
9
|
+
export function getModalityFromMimeType(mimeType: string): AttachmentModality {
|
|
10
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
11
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
12
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
13
|
+
return "document";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format a byte count as a human-readable file size string.
|
|
18
|
+
*/
|
|
19
|
+
export function formatFileSize(bytes: number): string {
|
|
20
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
21
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
22
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file exceeds the maximum allowed size.
|
|
27
|
+
*/
|
|
28
|
+
export function exceedsMaxSize(
|
|
29
|
+
file: File,
|
|
30
|
+
maxSize: number = DEFAULT_MAX_SIZE,
|
|
31
|
+
): boolean {
|
|
32
|
+
return file.size > maxSize;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a File as a base64 string (without the data URL prefix).
|
|
37
|
+
*/
|
|
38
|
+
export function readFileAsBase64(file: File): Promise<string> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const reader = new FileReader();
|
|
41
|
+
reader.onload = (e) => {
|
|
42
|
+
const result = e.target?.result as string;
|
|
43
|
+
const base64 = result?.split(",")[1];
|
|
44
|
+
if (base64) {
|
|
45
|
+
resolve(base64);
|
|
46
|
+
} else {
|
|
47
|
+
reject(new Error("Failed to read file as base64"));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
reader.onerror = reject;
|
|
51
|
+
reader.readAsDataURL(file);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a thumbnail data URL from a video file by capturing a frame near the start (at 0.1s).
|
|
57
|
+
* Returns undefined if thumbnail generation fails or if called outside a browser environment.
|
|
58
|
+
*/
|
|
59
|
+
export function generateVideoThumbnail(
|
|
60
|
+
file: File,
|
|
61
|
+
): Promise<string | undefined> {
|
|
62
|
+
if (typeof document === "undefined") {
|
|
63
|
+
return Promise.resolve(undefined);
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let resolved = false;
|
|
67
|
+
const video = document.createElement("video");
|
|
68
|
+
const canvas = document.createElement("canvas");
|
|
69
|
+
const url = URL.createObjectURL(file);
|
|
70
|
+
|
|
71
|
+
const cleanup = (result: string | undefined) => {
|
|
72
|
+
if (resolved) return;
|
|
73
|
+
resolved = true;
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
resolve(result);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[CopilotKit] generateVideoThumbnail: timed out for file "${file.name}"`,
|
|
81
|
+
);
|
|
82
|
+
cleanup(undefined);
|
|
83
|
+
}, 10000);
|
|
84
|
+
|
|
85
|
+
video.preload = "metadata";
|
|
86
|
+
video.muted = true;
|
|
87
|
+
video.playsInline = true;
|
|
88
|
+
|
|
89
|
+
video.onloadeddata = () => {
|
|
90
|
+
video.currentTime = 0.1;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
video.onseeked = () => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
canvas.width = video.videoWidth;
|
|
96
|
+
canvas.height = video.videoHeight;
|
|
97
|
+
const ctx = canvas.getContext("2d");
|
|
98
|
+
if (ctx) {
|
|
99
|
+
ctx.drawImage(video, 0, 0);
|
|
100
|
+
const thumbnail = canvas.toDataURL("image/jpeg", 0.7);
|
|
101
|
+
cleanup(thumbnail);
|
|
102
|
+
} else {
|
|
103
|
+
console.warn(
|
|
104
|
+
"[CopilotKit] generateVideoThumbnail: could not get 2d canvas context",
|
|
105
|
+
);
|
|
106
|
+
cleanup(undefined);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
video.onerror = () => {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
console.warn(
|
|
113
|
+
`[CopilotKit] generateVideoThumbnail: video element error for file "${file.name}"`,
|
|
114
|
+
);
|
|
115
|
+
cleanup(undefined);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
video.src = url;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a file's MIME type matches an accept filter string.
|
|
124
|
+
* Handles file extensions (e.g. ".pdf"), MIME wildcards ("image/*"), and comma-separated lists.
|
|
125
|
+
*/
|
|
126
|
+
export function matchesAcceptFilter(file: File, accept: string): boolean {
|
|
127
|
+
if (!accept || accept === "*/*") return true;
|
|
128
|
+
|
|
129
|
+
const filters = accept.split(",").map((f) => f.trim());
|
|
130
|
+
return filters.some((filter) => {
|
|
131
|
+
if (filter.startsWith(".")) {
|
|
132
|
+
return (file.name ?? "").toLowerCase().endsWith(filter.toLowerCase());
|
|
133
|
+
}
|
|
134
|
+
if (filter.endsWith("/*")) {
|
|
135
|
+
const prefix = filter.slice(0, -2);
|
|
136
|
+
return file.type.startsWith(prefix + "/");
|
|
137
|
+
}
|
|
138
|
+
return file.type === filter;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert an InputContentSource to a usable URL string.
|
|
144
|
+
* For data sources, returns a base64 data URL; for URL sources, returns the URL directly.
|
|
145
|
+
*/
|
|
146
|
+
export function getSourceUrl(source: InputContentSource): string {
|
|
147
|
+
if (source.type === "url") {
|
|
148
|
+
return source.value;
|
|
149
|
+
}
|
|
150
|
+
return `data:${source.mimeType};base64,${source.value}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Return a short human-readable label for a document MIME type (e.g. "PDF", "DOC").
|
|
155
|
+
*/
|
|
156
|
+
export function getDocumentIcon(mimeType: string): string {
|
|
157
|
+
if (mimeType.includes("pdf")) return "PDF";
|
|
158
|
+
if (mimeType.includes("sheet") || mimeType.includes("excel")) return "XLS";
|
|
159
|
+
if (mimeType.includes("presentation") || mimeType.includes("powerpoint"))
|
|
160
|
+
return "PPT";
|
|
161
|
+
if (mimeType.includes("word") || mimeType.includes("document")) return "DOC";
|
|
162
|
+
if (mimeType.includes("text/")) return "TXT";
|
|
163
|
+
return "FILE";
|
|
164
|
+
}
|