@assistant-ui/core 0.3.2 → 0.3.4
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/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +9 -7
- package/dist/internal.js +8 -6
- package/dist/react/primitive-hooks/useActionBarCopy.js +51 -24
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +6 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +26 -2
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +29 -8
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/runtime/api/bindings.d.ts +5 -0
- package/dist/runtime/api/bindings.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +6 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +7 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +11 -3
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +54 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +29 -23
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +101 -89
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +7 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +22 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +14 -1
- package/dist/utils/data-url.d.ts.map +1 -1
- package/dist/utils/data-url.js +23 -1
- package/dist/utils/data-url.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +9 -0
- package/dist/utils/getGroupStatus.d.ts.map +1 -0
- package/dist/utils/getGroupStatus.js +15 -0
- package/dist/utils/getGroupStatus.js.map +1 -0
- package/dist/utils/image-media-type.d.ts +13 -0
- package/dist/utils/image-media-type.d.ts.map +1 -0
- package/dist/utils/image-media-type.js +141 -0
- package/dist/utils/image-media-type.js.map +1 -0
- package/dist/utils/normalizePartStatus.d.ts +8 -0
- package/dist/utils/normalizePartStatus.d.ts.map +1 -0
- package/dist/utils/normalizePartStatus.js +39 -0
- package/dist/utils/normalizePartStatus.js.map +1 -0
- package/dist/utils/wire-media.d.ts +35 -0
- package/dist/utils/wire-media.d.ts.map +1 -0
- package/dist/utils/wire-media.js +55 -0
- package/dist/utils/wire-media.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +13 -1
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
- package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
- package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
- package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
- package/src/react/primitives/message/MessageParts.tsx +6 -1
- package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
- package/src/react/runtimes/cloud/auiV0.ts +59 -14
- package/src/react/types/MessagePartComponentTypes.ts +8 -0
- package/src/runtime/api/bindings.ts +5 -0
- package/src/runtime/api/message-runtime.test.ts +140 -0
- package/src/runtime/api/message-runtime.ts +10 -5
- package/src/runtime/api/thread-list-runtime.ts +22 -1
- package/src/runtime/api/thread-runtime.ts +15 -6
- package/src/runtime/base/base-composer-runtime-core.ts +72 -19
- package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
- package/src/runtimes/external-store/external-store-adapter.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
- package/src/store/clients/chain-of-thought-client.ts +2 -7
- package/src/store/clients/thread-message-client.test.ts +84 -0
- package/src/store/clients/thread-message-client.ts +14 -4
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
- package/src/store/scopes/thread-list-item.ts +7 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
- package/src/tests/auiV0Encode.test.ts +218 -1
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
- package/src/tests/event-subscription-listener-errors.test.ts +1 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +30 -0
- package/src/utils/data-url.ts +24 -0
- package/src/utils/getGroupStatus.test.ts +36 -0
- package/src/utils/getGroupStatus.ts +31 -0
- package/src/utils/image-media-type.test.ts +96 -0
- package/src/utils/image-media-type.ts +118 -0
- package/src/utils/normalizePartStatus.test.ts +59 -0
- package/src/utils/normalizePartStatus.ts +55 -0
- package/src/utils/wire-media.test.ts +96 -0
- package/src/utils/wire-media.ts +70 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/utils/image-media-type.ts
|
|
2
|
+
/**
|
|
3
|
+
* Leading-byte signatures for the image formats a browser or attachment
|
|
4
|
+
* adapter realistically produces. `null` matches any byte, which WebP needs
|
|
5
|
+
* because its four size bytes sit between the two markers, and AVIF and HEIC
|
|
6
|
+
* need because their leading `ftyp` box size varies by encoder.
|
|
7
|
+
*
|
|
8
|
+
* These are file format constants rather than an evolving API, so the table
|
|
9
|
+
* mirrors the one the AI SDK applies on the far side of several adapters
|
|
10
|
+
* without taking a dependency to reach it.
|
|
11
|
+
*/
|
|
12
|
+
const IMAGE_SIGNATURES = [
|
|
13
|
+
{
|
|
14
|
+
mediaType: "image/gif",
|
|
15
|
+
prefix: [
|
|
16
|
+
71,
|
|
17
|
+
73,
|
|
18
|
+
70
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
mediaType: "image/png",
|
|
23
|
+
prefix: [
|
|
24
|
+
137,
|
|
25
|
+
80,
|
|
26
|
+
78,
|
|
27
|
+
71
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
mediaType: "image/jpeg",
|
|
32
|
+
prefix: [255, 216]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
mediaType: "image/webp",
|
|
36
|
+
prefix: [
|
|
37
|
+
82,
|
|
38
|
+
73,
|
|
39
|
+
70,
|
|
40
|
+
70,
|
|
41
|
+
null,
|
|
42
|
+
null,
|
|
43
|
+
null,
|
|
44
|
+
null,
|
|
45
|
+
87,
|
|
46
|
+
69,
|
|
47
|
+
66,
|
|
48
|
+
80
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
mediaType: "image/bmp",
|
|
53
|
+
prefix: [66, 77]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
mediaType: "image/tiff",
|
|
57
|
+
prefix: [
|
|
58
|
+
73,
|
|
59
|
+
73,
|
|
60
|
+
42,
|
|
61
|
+
0
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
mediaType: "image/tiff",
|
|
66
|
+
prefix: [
|
|
67
|
+
77,
|
|
68
|
+
77,
|
|
69
|
+
0,
|
|
70
|
+
42
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
mediaType: "image/avif",
|
|
75
|
+
prefix: [
|
|
76
|
+
null,
|
|
77
|
+
null,
|
|
78
|
+
null,
|
|
79
|
+
null,
|
|
80
|
+
102,
|
|
81
|
+
116,
|
|
82
|
+
121,
|
|
83
|
+
112,
|
|
84
|
+
97,
|
|
85
|
+
118,
|
|
86
|
+
105,
|
|
87
|
+
102
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
mediaType: "image/heic",
|
|
92
|
+
prefix: [
|
|
93
|
+
null,
|
|
94
|
+
null,
|
|
95
|
+
null,
|
|
96
|
+
null,
|
|
97
|
+
102,
|
|
98
|
+
116,
|
|
99
|
+
121,
|
|
100
|
+
112,
|
|
101
|
+
104,
|
|
102
|
+
101,
|
|
103
|
+
105,
|
|
104
|
+
99
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
/** 16 base64 characters decode to the 12 bytes the longest signature needs. */
|
|
109
|
+
const PREFIX_CHARS = 16;
|
|
110
|
+
const decodePrefix = (base64) => {
|
|
111
|
+
const prefix = base64.slice(0, PREFIX_CHARS);
|
|
112
|
+
const aligned = prefix.slice(0, prefix.length - prefix.length % 4);
|
|
113
|
+
if (aligned.length === 0) return void 0;
|
|
114
|
+
const nodeBuffer = globalThis.Buffer;
|
|
115
|
+
if (nodeBuffer) return new Uint8Array(nodeBuffer.from(aligned, "base64"));
|
|
116
|
+
try {
|
|
117
|
+
const binary = atob(aligned);
|
|
118
|
+
const bytes = new Uint8Array(binary.length);
|
|
119
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
120
|
+
return bytes;
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The image media type a base64 payload's leading bytes identify, or
|
|
127
|
+
* `undefined` when they match no known signature or the input is not base64.
|
|
128
|
+
*
|
|
129
|
+
* An `ImageMessagePart` carries no media type, so an adapter that must declare
|
|
130
|
+
* one on the wire reads it from the bytes rather than assuming a format. Never
|
|
131
|
+
* throws: the input is whatever a caller put on the part.
|
|
132
|
+
*/
|
|
133
|
+
function detectImageMediaType(base64) {
|
|
134
|
+
const bytes = decodePrefix(base64);
|
|
135
|
+
if (!bytes) return void 0;
|
|
136
|
+
return IMAGE_SIGNATURES.find(({ prefix }) => bytes.length >= prefix.length && prefix.every((byte, index) => byte === null || bytes[index] === byte))?.mediaType;
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { detectImageMediaType };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=image-media-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-media-type.js","names":["IMAGE_SIGNATURES","mediaType","prefix","PREFIX_CHARS","decodePrefix","base64","Uint8Array","slice","aligned","length","undefined","nodeBuffer","globalThis","Buffer","from","data","encoding","binary","atob","bytes","i","charCodeAt","detectImageMediaType","find","every","byte","index"],"sources":["../../src/utils/image-media-type.ts"],"sourcesContent":["/**\n * Leading-byte signatures for the image formats a browser or attachment\n * adapter realistically produces. `null` matches any byte, which WebP needs\n * because its four size bytes sit between the two markers, and AVIF and HEIC\n * need because their leading `ftyp` box size varies by encoder.\n *\n * These are file format constants rather than an evolving API, so the table\n * mirrors the one the AI SDK applies on the far side of several adapters\n * without taking a dependency to reach it.\n */\nconst IMAGE_SIGNATURES: readonly {\n readonly mediaType: string;\n readonly prefix: readonly (number | null)[];\n}[] = [\n { mediaType: \"image/gif\", prefix: [0x47, 0x49, 0x46] },\n { mediaType: \"image/png\", prefix: [0x89, 0x50, 0x4e, 0x47] },\n { mediaType: \"image/jpeg\", prefix: [0xff, 0xd8] },\n {\n mediaType: \"image/webp\",\n prefix: [\n 0x52,\n 0x49,\n 0x46,\n 0x46,\n null,\n null,\n null,\n null,\n 0x57,\n 0x45,\n 0x42,\n 0x50,\n ],\n },\n { mediaType: \"image/bmp\", prefix: [0x42, 0x4d] },\n { mediaType: \"image/tiff\", prefix: [0x49, 0x49, 0x2a, 0x00] },\n { mediaType: \"image/tiff\", prefix: [0x4d, 0x4d, 0x00, 0x2a] },\n {\n mediaType: \"image/avif\",\n prefix: [\n null,\n null,\n null,\n null,\n 0x66,\n 0x74,\n 0x79,\n 0x70,\n 0x61,\n 0x76,\n 0x69,\n 0x66,\n ],\n },\n {\n mediaType: \"image/heic\",\n prefix: [\n null,\n null,\n null,\n null,\n 0x66,\n 0x74,\n 0x79,\n 0x70,\n 0x68,\n 0x65,\n 0x69,\n 0x63,\n ],\n },\n];\n\n/** 16 base64 characters decode to the 12 bytes the longest signature needs. */\nconst PREFIX_CHARS = 16;\n\nconst decodePrefix = (base64: string): Uint8Array | undefined => {\n const prefix = base64.slice(0, PREFIX_CHARS);\n const aligned = prefix.slice(0, prefix.length - (prefix.length % 4));\n if (aligned.length === 0) return undefined;\n\n const nodeBuffer = (\n globalThis as {\n Buffer?: {\n from(data: string, encoding: string): Uint8Array;\n };\n }\n ).Buffer;\n if (nodeBuffer) return new Uint8Array(nodeBuffer.from(aligned, \"base64\"));\n\n try {\n const binary = atob(aligned);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n } catch {\n return undefined;\n }\n};\n\n/**\n * The image media type a base64 payload's leading bytes identify, or\n * `undefined` when they match no known signature or the input is not base64.\n *\n * An `ImageMessagePart` carries no media type, so an adapter that must declare\n * one on the wire reads it from the bytes rather than assuming a format. Never\n * throws: the input is whatever a caller put on the part.\n */\nexport function detectImageMediaType(base64: string): string | undefined {\n const bytes = decodePrefix(base64);\n if (!bytes) return undefined;\n\n return IMAGE_SIGNATURES.find(\n ({ prefix }) =>\n bytes.length >= prefix.length &&\n prefix.every((byte, index) => byte === null || bytes[index] === byte),\n )?.mediaType;\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAMA,mBAGA;CACJ;EAAEC,WAAW;EAAaC,QAAQ;GAAC;GAAM;GAAM;EAAI;CAAE;CACrD;EAAED,WAAW;EAAaC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC3D;EAAED,WAAW;EAAcC,QAAQ,CAAC,KAAM,GAAI;CAAE;CAChD;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;CACA;EAAED,WAAW;EAAaC,QAAQ,CAAC,IAAM,EAAI;CAAE;CAC/C;EAAED,WAAW;EAAcC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC5D;EAAED,WAAW;EAAcC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC5D;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;CACA;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;AAAC;;AAIH,MAAMC,eAAe;AAErB,MAAMC,gBAAgBC,WAA2C;CAC/D,MAAMH,SAASG,OAAOE,MAAM,GAAGJ,YAAY;CAC3C,MAAMK,UAAUN,OAAOK,MAAM,GAAGL,OAAOO,SAAUP,OAAOO,SAAS,CAAE;CACnE,IAAID,QAAQC,WAAW,GAAG,OAAOC,KAAAA;CAEjC,MAAMC,aACJC,WAKAC;CACF,IAAIF,YAAY,OAAO,IAAIL,WAAWK,WAAWG,KAAKN,SAAS,QAAQ,CAAC;CAExE,IAAI;EACF,MAAMS,SAASC,KAAKV,OAAO;EAC3B,MAAMW,QAAQ,IAAIb,WAAWW,OAAOR,MAAM;EAC1C,KAAK,IAAIW,IAAI,GAAGA,IAAIH,OAAOR,QAAQW,KAAKD,MAAMC,KAAKH,OAAOI,WAAWD,CAAC;EACtE,OAAOD;CACT,QAAQ;EACN;CACF;AACF;;;;;;;;;AAUA,SAAgBG,qBAAqBjB,QAAoC;CACvE,MAAMc,QAAQf,aAAaC,MAAM;CACjC,IAAI,CAACc,OAAO,OAAOT,KAAAA;CAEnB,OAAOV,iBAAiBuB,MACrB,EAAErB,aACDiB,MAAMV,UAAUP,OAAOO,UACvBP,OAAOsB,OAAOC,MAAMC,UAAUD,SAAS,QAAQN,MAAMO,WAAWD,IAAI,CACxE,CAAC,EAAExB;AACL"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MessagePartStatus, ThreadAssistantMessagePart, ThreadUserMessagePart } from "../types/message.js";
|
|
2
|
+
//#region src/utils/normalizePartStatus.d.ts
|
|
3
|
+
declare const COMPLETE_STATUS: MessagePartStatus;
|
|
4
|
+
declare const RUNNING_STATUS: MessagePartStatus;
|
|
5
|
+
declare const normalizePartStatus: (part: ThreadUserMessagePart | ThreadAssistantMessagePart) => MessagePartStatus | undefined;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { COMPLETE_STATUS, RUNNING_STATUS, normalizePartStatus };
|
|
8
|
+
//# sourceMappingURL=normalizePartStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizePartStatus.d.ts","names":[],"sources":["../../src/utils/normalizePartStatus.ts"],"mappings":";;cAOa,iBAAiB;cAGjB,gBAAgB;cAoBhB,sBACX,MAAM,wBAAwB,+BAC7B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/utils/normalizePartStatus.ts
|
|
2
|
+
const COMPLETE_STATUS = Object.freeze({ type: "complete" });
|
|
3
|
+
const RUNNING_STATUS = Object.freeze({ type: "running" });
|
|
4
|
+
const INCOMPLETE_STATUSES = Object.freeze({
|
|
5
|
+
cancelled: Object.freeze({
|
|
6
|
+
type: "incomplete",
|
|
7
|
+
reason: "cancelled"
|
|
8
|
+
}),
|
|
9
|
+
length: Object.freeze({
|
|
10
|
+
type: "incomplete",
|
|
11
|
+
reason: "length"
|
|
12
|
+
}),
|
|
13
|
+
"content-filter": Object.freeze({
|
|
14
|
+
type: "incomplete",
|
|
15
|
+
reason: "content-filter"
|
|
16
|
+
}),
|
|
17
|
+
other: Object.freeze({
|
|
18
|
+
type: "incomplete",
|
|
19
|
+
reason: "other"
|
|
20
|
+
}),
|
|
21
|
+
error: Object.freeze({
|
|
22
|
+
type: "incomplete",
|
|
23
|
+
reason: "error"
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
const normalizePartStatus = (part) => {
|
|
27
|
+
const status = part.status;
|
|
28
|
+
if (!status || typeof status !== "object") return void 0;
|
|
29
|
+
const { type } = status;
|
|
30
|
+
if (type === "running") return RUNNING_STATUS;
|
|
31
|
+
if (type === "complete") return COMPLETE_STATUS;
|
|
32
|
+
if (type !== "incomplete") return void 0;
|
|
33
|
+
const { reason } = status;
|
|
34
|
+
return INCOMPLETE_STATUSES[reason === "cancelled" || reason === "length" || reason === "content-filter" || reason === "other" || reason === "error" ? reason : "other"];
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { COMPLETE_STATUS, RUNNING_STATUS, normalizePartStatus };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=normalizePartStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizePartStatus.js","names":["MessagePartStatus","MessagePartStreamStatus","ThreadAssistantMessagePart","ThreadUserMessagePart","COMPLETE_STATUS","Object","freeze","type","RUNNING_STATUS","IncompleteMessagePartStatus","Extract","INCOMPLETE_STATUSES","Readonly","Record","cancelled","reason","length","other","error","normalizePartStatus","part","status","undefined","normalizedReason"],"sources":["../../src/utils/normalizePartStatus.ts"],"sourcesContent":["import type {\n MessagePartStatus,\n MessagePartStreamStatus,\n ThreadAssistantMessagePart,\n ThreadUserMessagePart,\n} from \"../types/message\";\n\nexport const COMPLETE_STATUS: MessagePartStatus = Object.freeze({\n type: \"complete\",\n});\nexport const RUNNING_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\ntype IncompleteMessagePartStatus = Extract<\n MessagePartStreamStatus,\n { readonly type: \"incomplete\" }\n>;\nconst INCOMPLETE_STATUSES: Readonly<\n Record<IncompleteMessagePartStatus[\"reason\"], IncompleteMessagePartStatus>\n> = Object.freeze({\n cancelled: Object.freeze({ type: \"incomplete\", reason: \"cancelled\" }),\n length: Object.freeze({ type: \"incomplete\", reason: \"length\" }),\n \"content-filter\": Object.freeze({\n type: \"incomplete\",\n reason: \"content-filter\",\n }),\n other: Object.freeze({ type: \"incomplete\", reason: \"other\" }),\n error: Object.freeze({ type: \"incomplete\", reason: \"error\" }),\n});\n\nexport const normalizePartStatus = (\n part: ThreadUserMessagePart | ThreadAssistantMessagePart,\n): MessagePartStatus | undefined => {\n const status = (part as { readonly status?: unknown }).status;\n if (!status || typeof status !== \"object\") return undefined;\n\n const { type } = status as { readonly type?: unknown };\n if (type === \"running\") return RUNNING_STATUS;\n if (type === \"complete\") return COMPLETE_STATUS;\n if (type !== \"incomplete\") return undefined;\n\n const { reason } = status as {\n readonly reason?: unknown;\n };\n const normalizedReason: IncompleteMessagePartStatus[\"reason\"] =\n reason === \"cancelled\" ||\n reason === \"length\" ||\n reason === \"content-filter\" ||\n reason === \"other\" ||\n reason === \"error\"\n ? reason\n : \"other\";\n\n return INCOMPLETE_STATUSES[normalizedReason];\n};\n"],"mappings":";AAOA,MAAaI,kBAAqCC,OAAOC,OAAO,EAC9DC,MAAM,WACR,CAAC;AACD,MAAaC,iBAAoCH,OAAOC,OAAO,EAC7DC,MAAM,UACR,CAAC;AAKD,MAAMI,sBAEFN,OAAOC,OAAO;CAChBQ,WAAWT,OAAOC,OAAO;EAAEC,MAAM;EAAcQ,QAAQ;CAAY,CAAC;CACpEC,QAAQX,OAAOC,OAAO;EAAEC,MAAM;EAAcQ,QAAQ;CAAS,CAAC;CAC9D,kBAAkBV,OAAOC,OAAO;EAC9BC,MAAM;EACNQ,QAAQ;CACV,CAAC;CACDE,OAAOZ,OAAOC,OAAO;EAAEC,MAAM;EAAcQ,QAAQ;CAAQ,CAAC;CAC5DG,OAAOb,OAAOC,OAAO;EAAEC,MAAM;EAAcQ,QAAQ;CAAQ,CAAC;AAC9D,CAAC;AAED,MAAaI,uBACXC,SACkC;CAClC,MAAMC,SAAUD,KAAuCC;CACvD,IAAI,CAACA,UAAU,OAAOA,WAAW,UAAU,OAAOC,KAAAA;CAElD,MAAM,EAAEf,SAASc;CACjB,IAAId,SAAS,WAAW,OAAOC;CAC/B,IAAID,SAAS,YAAY,OAAOH;CAChC,IAAIG,SAAS,cAAc,OAAOe,KAAAA;CAElC,MAAM,EAAEP,WAAWM;CAYnB,OAAOV,oBARLI,WAAW,eACXA,WAAW,YACXA,WAAW,oBACXA,WAAW,WACXA,WAAW,UACPA,SACA;AAGR"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/utils/wire-media.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The media type to declare for an `ImageMessagePart`, which carries none of
|
|
4
|
+
* its own: an attachment's content type, then the payload's data URL
|
|
5
|
+
* declaration, then its leading bytes, then `image/png`.
|
|
6
|
+
*
|
|
7
|
+
* The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK
|
|
8
|
+
* for a url source and whenever inline bytes fail to sniff, so it fails exactly
|
|
9
|
+
* where a fallback is needed.
|
|
10
|
+
*/
|
|
11
|
+
declare function resolveImageMediaType(image: string, contentType?: string | undefined): string;
|
|
12
|
+
/**
|
|
13
|
+
* The media type to declare for a `FileMessagePart`: its own `mimeType`, then
|
|
14
|
+
* the payload's data URL declaration, then `application/octet-stream`.
|
|
15
|
+
*
|
|
16
|
+
* `mimeType` is a plain string, and an adapter reading `file.type` on a file
|
|
17
|
+
* the OS cannot type yields `""`, so the declared value is not always present.
|
|
18
|
+
*/
|
|
19
|
+
declare function resolveFileMediaType(data: string, mimeType?: string | undefined): string;
|
|
20
|
+
/**
|
|
21
|
+
* A payload placed into a wire field that is contractually a url.
|
|
22
|
+
*
|
|
23
|
+
* Two hazards drive this. A consumer may hand the value to an unguarded
|
|
24
|
+
* `new URL()`, so a payload that is not a url has to be wrapped; and a data
|
|
25
|
+
* URL's own media type wins over a separately declared one downstream, so a
|
|
26
|
+
* base64 envelope that disagrees with `mediaType` is rebuilt around the same
|
|
27
|
+
* bytes. Everything else passes through byte for byte: an envelope that agrees,
|
|
28
|
+
* a url of any other scheme, and a percent-encoded data URL, whose declaration
|
|
29
|
+
* cannot be corrected without transcoding the payload and is authoritative for
|
|
30
|
+
* the bytes it carries anyway.
|
|
31
|
+
*/
|
|
32
|
+
declare function toMediaWireUrl(payload: string, mediaType: string): string;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { resolveFileMediaType, resolveImageMediaType, toMediaWireUrl };
|
|
35
|
+
//# sourceMappingURL=wire-media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-media.d.ts","names":[],"sources":["../../src/utils/wire-media.ts"],"mappings":";;;;;;;;;;iBAYgB,sBACd,eACA;;;;;;;;iBA2Bc,qBACd,cACA;;;;;;;;;;;;;iBAiBc,eAAe,iBAAiB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { dataUrlMediaType, isParsableUrl, parseDataUrl } from "./data-url.js";
|
|
2
|
+
import { detectImageMediaType } from "./image-media-type.js";
|
|
3
|
+
//#region src/utils/wire-media.ts
|
|
4
|
+
/**
|
|
5
|
+
* The media type to declare for an `ImageMessagePart`, which carries none of
|
|
6
|
+
* its own: an attachment's content type, then the payload's data URL
|
|
7
|
+
* declaration, then its leading bytes, then `image/png`.
|
|
8
|
+
*
|
|
9
|
+
* The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK
|
|
10
|
+
* for a url source and whenever inline bytes fail to sniff, so it fails exactly
|
|
11
|
+
* where a fallback is needed.
|
|
12
|
+
*/
|
|
13
|
+
function resolveImageMediaType(image, contentType) {
|
|
14
|
+
if (contentType?.startsWith("image/")) return contentType;
|
|
15
|
+
const declared = dataUrlMediaType(image);
|
|
16
|
+
if (declared?.startsWith("image/")) return declared;
|
|
17
|
+
const payload = parseDataUrl(image)?.data ?? (isParsableUrl(image) ? void 0 : image);
|
|
18
|
+
if (payload !== void 0) {
|
|
19
|
+
const sniffed = detectImageMediaType(payload);
|
|
20
|
+
if (sniffed) return sniffed;
|
|
21
|
+
}
|
|
22
|
+
return "image/png";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The media type to declare for a `FileMessagePart`: its own `mimeType`, then
|
|
26
|
+
* the payload's data URL declaration, then `application/octet-stream`.
|
|
27
|
+
*
|
|
28
|
+
* `mimeType` is a plain string, and an adapter reading `file.type` on a file
|
|
29
|
+
* the OS cannot type yields `""`, so the declared value is not always present.
|
|
30
|
+
*/
|
|
31
|
+
function resolveFileMediaType(data, mimeType) {
|
|
32
|
+
return mimeType || dataUrlMediaType(data) || "application/octet-stream";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A payload placed into a wire field that is contractually a url.
|
|
36
|
+
*
|
|
37
|
+
* Two hazards drive this. A consumer may hand the value to an unguarded
|
|
38
|
+
* `new URL()`, so a payload that is not a url has to be wrapped; and a data
|
|
39
|
+
* URL's own media type wins over a separately declared one downstream, so a
|
|
40
|
+
* base64 envelope that disagrees with `mediaType` is rebuilt around the same
|
|
41
|
+
* bytes. Everything else passes through byte for byte: an envelope that agrees,
|
|
42
|
+
* a url of any other scheme, and a percent-encoded data URL, whose declaration
|
|
43
|
+
* cannot be corrected without transcoding the payload and is authoritative for
|
|
44
|
+
* the bytes it carries anyway.
|
|
45
|
+
*/
|
|
46
|
+
function toMediaWireUrl(payload, mediaType) {
|
|
47
|
+
const parsed = parseDataUrl(payload);
|
|
48
|
+
if (parsed) return parsed.mimeType === mediaType ? payload : `data:${mediaType};base64,${parsed.data}`;
|
|
49
|
+
if (isParsableUrl(payload)) return payload;
|
|
50
|
+
return `data:${mediaType};base64,${payload}`;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { resolveFileMediaType, resolveImageMediaType, toMediaWireUrl };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=wire-media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\")) return contentType;\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\")) return declared;\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,GAAG,OAAOD;CAE9C,MAAME,WAAWR,iBAAiBK,KAAK;CACvC,IAAIG,UAAUD,WAAW,QAAQ,GAAG,OAAOC;CAM3C,MAAME,UADSR,aAAaG,KACZI,CAAM,EAAEE,SAASV,cAAcI,KAAK,IAAIO,KAAAA,IAAYP;CACpE,IAAIK,YAAYE,KAAAA,GAAW;EACzB,MAAMC,UAAUV,qBAAqBO,OAAO;EAC5C,IAAIG,SAAS,OAAOA;CACtB;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,qBACdH,MACAI,UACQ;CACR,OAAOA,YAAYf,iBAAiBW,IAAI,KAAK;AAC/C;;;;;;;;;;;;;AAcA,SAAgBK,eAAeN,SAAiBO,WAA2B;CACzE,MAAMR,SAASP,aAAaQ,OAAO;CACnC,IAAID,QACF,OAAOA,OAAOM,aAAaE,YACvBP,UACA,QAAQO,UAAS,UAAWR,OAAOE;CAEzC,IAAIV,cAAcS,OAAO,GAAG,OAAOA;CACnC,OAAO,QAAQO,UAAS,UAAWP;AACrC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Framework-agnostic core runtime for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"sideEffects": false,
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
62
|
+
"assistant-stream": "^0.3.32",
|
|
63
63
|
"nanoid": "^6.0.0"
|
|
64
64
|
},
|
|
65
65
|
"optionalDevDependencies": {
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
"react-dom": "^19.2.8",
|
|
96
96
|
"vitest": "^4.1.10",
|
|
97
97
|
"zustand": "^5.0.14",
|
|
98
|
-
"@assistant-ui/store": "0.3.
|
|
99
|
-
"@assistant-ui/tap": "0.9.
|
|
98
|
+
"@assistant-ui/store": "0.3.3",
|
|
99
|
+
"@assistant-ui/tap": "0.9.9",
|
|
100
100
|
"@assistant-ui/vite": "0.0.11",
|
|
101
101
|
"@assistant-ui/x-buildutils": "0.0.20",
|
|
102
102
|
"assistant-cloud": "0.1.37"
|
package/src/index.ts
CHANGED
package/src/internal.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
|
|
33
33
|
// Message utilities
|
|
34
34
|
export { getThreadMessageText } from "./utils/text";
|
|
35
|
+
export { toMessagePartStatus } from "./runtime/api/message-runtime";
|
|
35
36
|
export { resolveToolApprovalResponse } from "./runtime/utils/resolveToolApprovalResponse";
|
|
36
37
|
export { consumeSuggestionResult } from "./adapters/suggestion";
|
|
37
38
|
|
|
@@ -56,7 +57,18 @@ export { isJSONValue, isRecord } from "./utils/json/is-json";
|
|
|
56
57
|
|
|
57
58
|
// Data-URL decoder and http(s) matcher, reused by framework adapters so the
|
|
58
59
|
// outbound part conversion lives in one place.
|
|
59
|
-
export {
|
|
60
|
+
export {
|
|
61
|
+
dataUrlMediaType,
|
|
62
|
+
httpUrlPattern,
|
|
63
|
+
isParsableUrl,
|
|
64
|
+
parseDataUrl,
|
|
65
|
+
} from "./utils/data-url";
|
|
66
|
+
export { detectImageMediaType } from "./utils/image-media-type";
|
|
67
|
+
export {
|
|
68
|
+
resolveFileMediaType,
|
|
69
|
+
resolveImageMediaType,
|
|
70
|
+
toMediaWireUrl,
|
|
71
|
+
} from "./utils/wire-media";
|
|
60
72
|
|
|
61
73
|
export * from "./runtime/internal";
|
|
62
74
|
export * from "./runtimes/internal";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { cleanup, renderHook } from "@testing-library/react";
|
|
1
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
4
|
|
|
3
5
|
const mocks = vi.hoisted(() => {
|
|
@@ -23,18 +25,18 @@ const mocks = vi.hoisted(() => {
|
|
|
23
25
|
setIsCopied,
|
|
24
26
|
},
|
|
25
27
|
},
|
|
28
|
+
currentAui: {
|
|
29
|
+
message: {
|
|
30
|
+
getCopyText: () => "Hello",
|
|
31
|
+
setIsCopied,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
26
34
|
};
|
|
27
35
|
});
|
|
28
36
|
|
|
29
|
-
vi.mock("react", async (importOriginal) => ({
|
|
30
|
-
...(await importOriginal<typeof import("react")>()),
|
|
31
|
-
useCallback: ((callback: unknown) =>
|
|
32
|
-
callback) as typeof import("react").useCallback,
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
37
|
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
36
38
|
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
37
|
-
useAui: () => mocks.
|
|
39
|
+
useAui: () => mocks.currentAui,
|
|
38
40
|
useAuiState: ((selector: (state: typeof mocks.state) => unknown) =>
|
|
39
41
|
selector(mocks.state)) as typeof import("@assistant-ui/store").useAuiState,
|
|
40
42
|
}));
|
|
@@ -42,25 +44,28 @@ vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
|
42
44
|
import { useActionBarCopy } from "./useActionBarCopy";
|
|
43
45
|
|
|
44
46
|
afterEach(() => {
|
|
47
|
+
cleanup();
|
|
45
48
|
vi.clearAllMocks();
|
|
49
|
+
vi.useRealTimers();
|
|
50
|
+
mocks.currentAui = mocks.aui;
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
describe("useActionBarCopy", () => {
|
|
49
54
|
it("does not report copy success without a clipboard handler", async () => {
|
|
50
|
-
const {
|
|
55
|
+
const { result } = renderHook(() => useActionBarCopy());
|
|
51
56
|
|
|
52
|
-
copy();
|
|
57
|
+
result.current.copy();
|
|
53
58
|
await Promise.resolve();
|
|
54
59
|
|
|
55
|
-
expect(disabled).toBe(true);
|
|
60
|
+
expect(result.current.disabled).toBe(true);
|
|
56
61
|
expect(mocks.setIsCopied).not.toHaveBeenCalled();
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
it("reports copy success after the clipboard handler resolves", async () => {
|
|
60
65
|
const copyToClipboard = vi.fn();
|
|
61
|
-
const {
|
|
66
|
+
const { result } = renderHook(() => useActionBarCopy({ copyToClipboard }));
|
|
62
67
|
|
|
63
|
-
copy();
|
|
68
|
+
result.current.copy();
|
|
64
69
|
await Promise.resolve();
|
|
65
70
|
|
|
66
71
|
expect(copyToClipboard).toHaveBeenCalledWith("Hello");
|
|
@@ -69,11 +74,101 @@ describe("useActionBarCopy", () => {
|
|
|
69
74
|
|
|
70
75
|
it("does not report copy success when the clipboard handler rejects", async () => {
|
|
71
76
|
const copyToClipboard = vi.fn().mockRejectedValue(new Error("denied"));
|
|
72
|
-
const {
|
|
77
|
+
const { result } = renderHook(() => useActionBarCopy({ copyToClipboard }));
|
|
78
|
+
|
|
79
|
+
result.current.copy();
|
|
80
|
+
await Promise.resolve();
|
|
81
|
+
|
|
82
|
+
expect(mocks.setIsCopied).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps copy success visible for the full duration after copying again", async () => {
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
const copyToClipboard = vi.fn();
|
|
88
|
+
const { result } = renderHook(() =>
|
|
89
|
+
useActionBarCopy({
|
|
90
|
+
copiedDuration: 3000,
|
|
91
|
+
copyToClipboard,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
result.current.copy();
|
|
96
|
+
await Promise.resolve();
|
|
97
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
98
|
+
|
|
99
|
+
result.current.copy();
|
|
100
|
+
await Promise.resolve();
|
|
101
|
+
mocks.setIsCopied.mockClear();
|
|
102
|
+
|
|
103
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
104
|
+
expect(mocks.setIsCopied).not.toHaveBeenCalledWith(false);
|
|
73
105
|
|
|
74
|
-
|
|
106
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
107
|
+
expect(mocks.setIsCopied).toHaveBeenCalledWith(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("resets copy feedback when unmounted", async () => {
|
|
111
|
+
vi.useFakeTimers();
|
|
112
|
+
const copyToClipboard = vi.fn();
|
|
113
|
+
const { result, unmount } = renderHook(() =>
|
|
114
|
+
useActionBarCopy({ copyToClipboard }),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
result.current.copy();
|
|
118
|
+
await Promise.resolve();
|
|
119
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
120
|
+
|
|
121
|
+
mocks.setIsCopied.mockClear();
|
|
122
|
+
unmount();
|
|
123
|
+
|
|
124
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
125
|
+
expect(mocks.setIsCopied).toHaveBeenCalledWith(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ignores clipboard success after unmount", async () => {
|
|
129
|
+
vi.useFakeTimers();
|
|
130
|
+
let resolveCopy: (() => void) | undefined;
|
|
131
|
+
const copyToClipboard = vi.fn(
|
|
132
|
+
() =>
|
|
133
|
+
new Promise<void>((resolve) => {
|
|
134
|
+
resolveCopy = resolve;
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
const { result, unmount } = renderHook(() =>
|
|
138
|
+
useActionBarCopy({ copyToClipboard }),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
result.current.copy();
|
|
142
|
+
unmount();
|
|
143
|
+
resolveCopy?.();
|
|
75
144
|
await Promise.resolve();
|
|
76
145
|
|
|
77
146
|
expect(mocks.setIsCopied).not.toHaveBeenCalled();
|
|
147
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("resets feedback for the previous message scope", async () => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
const copyToClipboard = vi.fn();
|
|
153
|
+
const nextSetIsCopied = vi.fn();
|
|
154
|
+
const { result, rerender } = renderHook(() =>
|
|
155
|
+
useActionBarCopy({ copyToClipboard }),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
result.current.copy();
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
mocks.setIsCopied.mockClear();
|
|
161
|
+
|
|
162
|
+
mocks.currentAui = {
|
|
163
|
+
message: {
|
|
164
|
+
getCopyText: () => "Next message",
|
|
165
|
+
setIsCopied: nextSetIsCopied,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
rerender();
|
|
169
|
+
|
|
170
|
+
expect(mocks.setIsCopied).toHaveBeenCalledWith(false);
|
|
171
|
+
expect(nextSetIsCopied).not.toHaveBeenCalled();
|
|
172
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
78
173
|
});
|
|
79
174
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
3
3
|
|
|
4
4
|
export type UseActionBarCopyOptions = {
|
|
@@ -21,19 +21,44 @@ export const useActionBarCopy = ({
|
|
|
21
21
|
const isCopied = useAuiState((s) => s.message.isCopied);
|
|
22
22
|
const isEditing = useAuiState((s) => s.composer.isEditing);
|
|
23
23
|
const composerValue = useAuiState((s) => s.composer.text);
|
|
24
|
+
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
25
|
+
undefined,
|
|
26
|
+
);
|
|
27
|
+
const scopeGenerationRef = useRef(0);
|
|
28
|
+
|
|
29
|
+
useEffect(
|
|
30
|
+
() => () => {
|
|
31
|
+
scopeGenerationRef.current += 1;
|
|
32
|
+
if (copiedTimerRef.current === undefined) return;
|
|
33
|
+
|
|
34
|
+
clearTimeout(copiedTimerRef.current);
|
|
35
|
+
copiedTimerRef.current = undefined;
|
|
36
|
+
aui.message.setIsCopied(false);
|
|
37
|
+
},
|
|
38
|
+
[aui],
|
|
39
|
+
);
|
|
24
40
|
|
|
25
41
|
const copy = useCallback(() => {
|
|
26
42
|
if (!copyToClipboard) return;
|
|
27
43
|
|
|
28
44
|
const valueToCopy = isEditing ? composerValue : aui.message.getCopyText();
|
|
29
45
|
if (!valueToCopy) return;
|
|
46
|
+
const scopeGeneration = scopeGenerationRef.current;
|
|
30
47
|
|
|
31
48
|
// The rejection handler swallows clipboard write failures (permission denied,
|
|
32
49
|
// API unavailable) so they don't surface as unhandled promise rejections.
|
|
33
50
|
Promise.resolve(copyToClipboard(valueToCopy)).then(
|
|
34
51
|
() => {
|
|
52
|
+
if (scopeGeneration !== scopeGenerationRef.current) return;
|
|
53
|
+
|
|
54
|
+
if (copiedTimerRef.current !== undefined) {
|
|
55
|
+
clearTimeout(copiedTimerRef.current);
|
|
56
|
+
}
|
|
35
57
|
aui.message.setIsCopied(true);
|
|
36
|
-
setTimeout(() =>
|
|
58
|
+
copiedTimerRef.current = setTimeout(() => {
|
|
59
|
+
copiedTimerRef.current = undefined;
|
|
60
|
+
aui.message.setIsCopied(false);
|
|
61
|
+
}, copiedDuration);
|
|
37
62
|
},
|
|
38
63
|
() => {},
|
|
39
64
|
);
|