@athenaintel/react 0.39.0 → 0.40.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/README.md +9 -1
- package/dist/agent-runtime/src/errors.cjs +8 -0
- package/dist/agent-runtime/src/errors.cjs.map +1 -1
- package/dist/agent-runtime/src/errors.js +8 -0
- package/dist/agent-runtime/src/errors.js.map +1 -1
- package/dist/agent-runtime/src/messages.cjs +76 -16
- package/dist/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/agent-runtime/src/messages.js +76 -16
- package/dist/agent-runtime/src/messages.js.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.cjs +15 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +15 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/assets/use-asset-content.cjs +127 -0
- package/dist/assets/use-asset-content.cjs.map +1 -0
- package/dist/assets/use-asset-content.d.ts +60 -0
- package/dist/assets/use-asset-content.js +127 -0
- package/dist/assets/use-asset-content.js.map +1 -0
- package/dist/chat/super-grouping/CollapsibleGroup.cjs +16 -8
- package/dist/chat/super-grouping/CollapsibleGroup.cjs.map +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.d.ts +26 -1
- package/dist/chat/super-grouping/CollapsibleGroup.js +16 -8
- package/dist/chat/super-grouping/CollapsibleGroup.js.map +1 -1
- package/dist/chat/super-grouping/GroupedMessageParts.cjs +59 -0
- package/dist/chat/super-grouping/GroupedMessageParts.cjs.map +1 -0
- package/dist/chat/super-grouping/GroupedMessageParts.d.ts +25 -0
- package/dist/chat/super-grouping/GroupedMessageParts.js +59 -0
- package/dist/chat/super-grouping/GroupedMessageParts.js.map +1 -0
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +9 -5
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.d.ts +2 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +13 -9
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +10 -6
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +14 -10
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/grouped-part-keys.cjs +104 -0
- package/dist/chat/super-grouping/grouped-part-keys.cjs.map +1 -0
- package/dist/chat/super-grouping/grouped-part-keys.d.ts +37 -0
- package/dist/chat/super-grouping/grouped-part-keys.js +104 -0
- package/dist/chat/super-grouping/grouped-part-keys.js.map +1 -0
- package/dist/chat/super-grouping/index.cjs +2 -0
- package/dist/chat/super-grouping/index.cjs.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +3 -1
- package/dist/chat/super-grouping/index.js +2 -0
- package/dist/chat/super-grouping/index.js.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.cjs +25 -19
- package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.d.ts +5 -0
- package/dist/chat-ui/composer/tiptap-composer.js +26 -20
- package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
- package/dist/composer/clipboard.cjs +16 -0
- package/dist/composer/clipboard.cjs.map +1 -1
- package/dist/composer/clipboard.d.ts +9 -0
- package/dist/composer/clipboard.js +17 -1
- package/dist/composer/clipboard.js.map +1 -1
- package/dist/diagnostics/errors.cjs +6 -0
- package/dist/diagnostics/errors.cjs.map +1 -1
- package/dist/diagnostics/errors.d.ts +6 -0
- package/dist/diagnostics/errors.js +6 -0
- package/dist/diagnostics/errors.js.map +1 -1
- package/dist/index.cjs +23 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/statewire-lifecycle.cjs +8 -0
- package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
- package/dist/runtime/statewire-lifecycle.js +9 -1
- package/dist/runtime/statewire-lifecycle.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tools/index.cjs +7 -0
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inline-tools.cjs +10 -0
- package/dist/tools/inline-tools.cjs.map +1 -0
- package/dist/tools/inline-tools.d.ts +2 -0
- package/dist/tools/inline-tools.js +10 -0
- package/dist/tools/inline-tools.js.map +1 -0
- package/dist/tools/test-recording-report.cjs +175 -0
- package/dist/tools/test-recording-report.cjs.map +1 -0
- package/dist/tools/test-recording-report.d.ts +82 -0
- package/dist/tools/test-recording-report.js +175 -0
- package/dist/tools/test-recording-report.js.map +1 -0
- package/dist/tools/test-recording-tool-uis.cjs +498 -0
- package/dist/tools/test-recording-tool-uis.cjs.map +1 -0
- package/dist/tools/test-recording-tool-uis.d.ts +32 -0
- package/dist/tools/test-recording-tool-uis.js +498 -0
- package/dist/tools/test-recording-tool-uis.js.map +1 -0
- package/dist/tools/tool-summary.cjs +49 -1
- package/dist/tools/tool-summary.cjs.map +1 -1
- package/dist/tools/tool-summary.js +49 -1
- package/dist/tools/tool-summary.js.map +1 -1
- package/dist/tools/tool-uis.cjs +6 -1
- package/dist/tools/tool-uis.cjs.map +1 -1
- package/dist/tools/tool-uis.js +6 -1
- package/dist/tools/tool-uis.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const reactQuery = require("@tanstack/react-query");
|
|
4
|
+
const react = require("react");
|
|
5
|
+
const mint = require("../collab/mint.cjs");
|
|
6
|
+
const errors = require("../diagnostics/errors.cjs");
|
|
7
|
+
const useAthenaRestQuery = require("../lib/use-athena-rest-query.cjs");
|
|
8
|
+
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
9
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
10
|
+
function athenaAssetContentPath(assetId) {
|
|
11
|
+
return `/api/asset/content/${encodeURIComponent(assetId)}`;
|
|
12
|
+
}
|
|
13
|
+
function isAbortError(error) {
|
|
14
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
15
|
+
}
|
|
16
|
+
function assetContentError({
|
|
17
|
+
error,
|
|
18
|
+
assetId,
|
|
19
|
+
message,
|
|
20
|
+
hint
|
|
21
|
+
}) {
|
|
22
|
+
if (isAbortError(error) || errors.isAthenaSdkError(error)) return error;
|
|
23
|
+
return new errors.AthenaSdkError({
|
|
24
|
+
code: "asset_content_failed",
|
|
25
|
+
message,
|
|
26
|
+
cause: error,
|
|
27
|
+
hint,
|
|
28
|
+
context: { assetId }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function fetchAthenaAssetContent({
|
|
32
|
+
backendUrl,
|
|
33
|
+
auth,
|
|
34
|
+
assetId,
|
|
35
|
+
signal
|
|
36
|
+
}) {
|
|
37
|
+
const url = `${mint.getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;
|
|
38
|
+
let response;
|
|
39
|
+
try {
|
|
40
|
+
response = await credentialRenewal.sendWithCredentialRenewal(
|
|
41
|
+
auth,
|
|
42
|
+
(headers) => fetch(url, { method: "GET", headers, signal: signal ?? null })
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw assetContentError({
|
|
46
|
+
error,
|
|
47
|
+
assetId,
|
|
48
|
+
message: "Reading the asset could not reach the Agora API, or its credential could not be resolved.",
|
|
49
|
+
hint: "Check the network, that backendUrl points at the Agora API base, and that getToken resolves."
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const refused = response.status === 401 || response.status === 403;
|
|
54
|
+
throw new errors.AthenaSdkError({
|
|
55
|
+
code: refused ? "auth_rejected" : "asset_content_failed",
|
|
56
|
+
message: `Reading the asset failed: ${response.status}`,
|
|
57
|
+
status: response.status,
|
|
58
|
+
detail: await response.json().catch(() => void 0),
|
|
59
|
+
hint: refused ? "The signed-in user cannot read this asset." : response.status === 404 ? "The asset has no stored file, or it no longer exists." : "Check that backendUrl points at the Agora API base.",
|
|
60
|
+
context: { assetId }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return await response.blob();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw assetContentError({
|
|
67
|
+
error,
|
|
68
|
+
assetId,
|
|
69
|
+
message: "Reading the asset failed while its file downloaded.",
|
|
70
|
+
hint: "Check the network and try again."
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const NO_RETRY_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 409]);
|
|
75
|
+
function retryTransient(failureCount, error) {
|
|
76
|
+
const status = error.status;
|
|
77
|
+
if (typeof status === "number" && NO_RETRY_STATUSES.has(status)) return false;
|
|
78
|
+
return failureCount < 1;
|
|
79
|
+
}
|
|
80
|
+
function useAthenaAssetContent({
|
|
81
|
+
assetId,
|
|
82
|
+
enabled = true
|
|
83
|
+
}) {
|
|
84
|
+
const config = AthenaContext.useAthenaConfig();
|
|
85
|
+
const { backendUrl, apiKey, getToken } = config;
|
|
86
|
+
const identity = useAthenaRestQuery.useAthenaQueryIdentity({ apiKey, token: config.token, getToken });
|
|
87
|
+
const queryClient = useAthenaRestQuery.useAthenaQueryClient();
|
|
88
|
+
const reader = react.useId();
|
|
89
|
+
return reactQuery.useQuery(
|
|
90
|
+
{
|
|
91
|
+
queryKey: ["athena-sdk", "asset-content", backendUrl, ...identity, assetId, reader],
|
|
92
|
+
// `config.token` is read at fetch time: the provider serves the latest
|
|
93
|
+
// token through a getter, so rotations reach the request.
|
|
94
|
+
queryFn: ({ signal }) => fetchAthenaAssetContent({
|
|
95
|
+
backendUrl,
|
|
96
|
+
auth: { apiKey, token: config.token, getToken },
|
|
97
|
+
assetId: assetId ?? "",
|
|
98
|
+
signal
|
|
99
|
+
}),
|
|
100
|
+
enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,
|
|
101
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
102
|
+
gcTime: 0,
|
|
103
|
+
refetchOnWindowFocus: false,
|
|
104
|
+
refetchOnReconnect: false,
|
|
105
|
+
retry: retryTransient
|
|
106
|
+
},
|
|
107
|
+
queryClient
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function useObjectUrl(blob) {
|
|
111
|
+
const [url, setUrl] = react.useState(null);
|
|
112
|
+
react.useEffect(() => {
|
|
113
|
+
if (!blob) {
|
|
114
|
+
setUrl(null);
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
118
|
+
setUrl(objectUrl);
|
|
119
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
120
|
+
}, [blob]);
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
123
|
+
exports.athenaAssetContentPath = athenaAssetContentPath;
|
|
124
|
+
exports.fetchAthenaAssetContent = fetchAthenaAssetContent;
|
|
125
|
+
exports.useAthenaAssetContent = useAthenaAssetContent;
|
|
126
|
+
exports.useObjectUrl = useObjectUrl;
|
|
127
|
+
//# sourceMappingURL=use-asset-content.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-content.cjs","sources":["../../src/assets/use-asset-content.ts"],"sourcesContent":["/**\n * An asset's stored file, read with the signed-in user's own credential:\n * `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`\n * `serve_asset_content`), which passes the asset's permission check\n * (`get_asset_with_permission_check`) and streams the bytes through Agora.\n * No link is minted — no presigned storage URL, no embed token — so nothing\n * that could be opened without the credential ever exists: the bytes land in\n * a `Blob` that only this page holds, and a media element plays it through an\n * object URL (`useObjectUrl`).\n *\n * This is how the SDK plays an asset's video: a `<video src>` cannot carry\n * the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the\n * alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)\n * each hand out a URL that works for whoever holds it. The whole file is read\n * before playback starts, so read it when the person asks to play, not on\n * mount.\n */\nimport { useQuery, type UseQueryResult } from '@tanstack/react-query';\nimport { useEffect, useId, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError, isAthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaQueryClient, useAthenaQueryIdentity } from '../lib/use-athena-rest-query';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\n\n/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */\nexport function athenaAssetContentPath(assetId: string): string {\n return `/api/asset/content/${encodeURIComponent(assetId)}`;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/** `error` as an `asset_content_failed` SDK error; an abort or an SDK error passes through. */\nfunction assetContentError({\n error,\n assetId,\n message,\n hint,\n}: {\n error: unknown;\n assetId: string;\n message: string;\n hint: string;\n}): unknown {\n if (isAbortError(error) || isAthenaSdkError(error)) return error;\n return new AthenaSdkError({\n code: 'asset_content_failed',\n message,\n cause: error,\n hint,\n context: { assetId },\n });\n}\n\n/**\n * Read an asset's stored file as a `Blob`. The credential rides\n * `Authorization: Bearer` (resolved live through `getToken` when the host gave\n * one) or `X-API-KEY`; a refused bearer (`401`) is renewed once\n * (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —\n * `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP\n * `status` when the server answered), a credential that could not be\n * resolved and a download cut off mid-body included; only an abort keeps its\n * own identity.\n */\nexport async function fetchAthenaAssetContent({\n backendUrl,\n auth,\n assetId,\n signal,\n}: {\n backendUrl: string;\n auth: StatewireAuthSource;\n assetId: string;\n signal?: AbortSignal;\n}): Promise<Blob> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;\n let response: Response;\n try {\n response = await sendWithCredentialRenewal(auth, (headers) =>\n fetch(url, { method: 'GET', headers, signal: signal ?? null }),\n );\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset could not reach the Agora API, or its credential could not be resolved.',\n hint: 'Check the network, that backendUrl points at the Agora API base, and that getToken resolves.',\n });\n }\n if (!response.ok) {\n const refused = response.status === 401 || response.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'asset_content_failed',\n message: `Reading the asset failed: ${response.status}`,\n status: response.status,\n detail: await response.json().catch(() => undefined),\n hint: refused\n ? 'The signed-in user cannot read this asset.'\n : response.status === 404\n ? 'The asset has no stored file, or it no longer exists.'\n : 'Check that backendUrl points at the Agora API base.',\n context: { assetId },\n });\n }\n try {\n return await response.blob();\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset failed while its file downloaded.',\n hint: 'Check the network and try again.',\n });\n }\n}\n\nconst NO_RETRY_STATUSES: ReadonlySet<number> = new Set([400, 401, 403, 404, 409]);\n\n/** Retry a transient failure once; a refusal or a missing file answers the same way again. */\nfunction retryTransient(failureCount: number, error: Error): boolean {\n const status = (error as { status?: unknown }).status;\n if (typeof status === 'number' && NO_RETRY_STATUSES.has(status)) return false;\n return failureCount < 1;\n}\n\nexport interface UseAthenaAssetContentOptions {\n /** The asset to read; `null` holds the query idle. */\n assetId: string | null;\n /** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * An asset's stored file through TanStack Query, with the provider's\n * `backendUrl` and credentials. Each mounted hook reads its own copy: the key\n * carries the hook instance as well as the asset, the signed-in principal and\n * a fingerprint of the credential (`useAthenaQueryIdentity`, never the\n * credential itself), and the bytes are dropped as soon as it unmounts. So no\n * reader is ever handed bytes another mount read — not even behind a host\n * `getToken`, whose account the key cannot see — and every new reader passes\n * the permission check again. While mounted the file is read once (a stored\n * file does not change under its asset id); `refetch()` reads it again.\n */\nexport function useAthenaAssetContent({\n assetId,\n enabled = true,\n}: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });\n const queryClient = useAthenaQueryClient();\n const reader = useId();\n return useQuery(\n {\n queryKey: ['athena-sdk', 'asset-content', backendUrl, ...identity, assetId, reader],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchAthenaAssetContent({\n backendUrl,\n auth: { apiKey, token: config.token, getToken },\n assetId: assetId ?? '',\n signal,\n }),\n enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,\n staleTime: Number.POSITIVE_INFINITY,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: retryTransient,\n },\n queryClient,\n );\n}\n\n/**\n * An object URL for `blob` while it is shown, revoked when the blob changes\n * or the component unmounts; `null` without a blob.\n */\nexport function useObjectUrl(blob: Blob | null | undefined): string | null {\n const [url, setUrl] = useState<string | null>(null);\n useEffect(() => {\n if (!blob) {\n setUrl(null);\n return undefined;\n }\n const objectUrl = URL.createObjectURL(blob);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [blob]);\n return url;\n}\n"],"names":["isAthenaSdkError","AthenaSdkError","getAthenaApiBaseUrl","sendWithCredentialRenewal","useAthenaConfig","useAthenaQueryIdentity","useAthenaQueryClient","useId","useQuery","useState","useEffect"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An asset's stored file, read with the signed-in user's own credential:
|
|
3
|
+
* `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`
|
|
4
|
+
* `serve_asset_content`), which passes the asset's permission check
|
|
5
|
+
* (`get_asset_with_permission_check`) and streams the bytes through Agora.
|
|
6
|
+
* No link is minted — no presigned storage URL, no embed token — so nothing
|
|
7
|
+
* that could be opened without the credential ever exists: the bytes land in
|
|
8
|
+
* a `Blob` that only this page holds, and a media element plays it through an
|
|
9
|
+
* object URL (`useObjectUrl`).
|
|
10
|
+
*
|
|
11
|
+
* This is how the SDK plays an asset's video: a `<video src>` cannot carry
|
|
12
|
+
* the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the
|
|
13
|
+
* alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)
|
|
14
|
+
* each hand out a URL that works for whoever holds it. The whole file is read
|
|
15
|
+
* before playback starts, so read it when the person asks to play, not on
|
|
16
|
+
* mount.
|
|
17
|
+
*/
|
|
18
|
+
import { type UseQueryResult } from '@tanstack/react-query';
|
|
19
|
+
import type { StatewireAuthSource } from '../runtime/statewire-auth-headers';
|
|
20
|
+
/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */
|
|
21
|
+
export declare function athenaAssetContentPath(assetId: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Read an asset's stored file as a `Blob`. The credential rides
|
|
24
|
+
* `Authorization: Bearer` (resolved live through `getToken` when the host gave
|
|
25
|
+
* one) or `X-API-KEY`; a refused bearer (`401`) is renewed once
|
|
26
|
+
* (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —
|
|
27
|
+
* `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP
|
|
28
|
+
* `status` when the server answered), a credential that could not be
|
|
29
|
+
* resolved and a download cut off mid-body included; only an abort keeps its
|
|
30
|
+
* own identity.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fetchAthenaAssetContent({ backendUrl, auth, assetId, signal, }: {
|
|
33
|
+
backendUrl: string;
|
|
34
|
+
auth: StatewireAuthSource;
|
|
35
|
+
assetId: string;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}): Promise<Blob>;
|
|
38
|
+
export interface UseAthenaAssetContentOptions {
|
|
39
|
+
/** The asset to read; `null` holds the query idle. */
|
|
40
|
+
assetId: string | null;
|
|
41
|
+
/** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An asset's stored file through TanStack Query, with the provider's
|
|
46
|
+
* `backendUrl` and credentials. Each mounted hook reads its own copy: the key
|
|
47
|
+
* carries the hook instance as well as the asset, the signed-in principal and
|
|
48
|
+
* a fingerprint of the credential (`useAthenaQueryIdentity`, never the
|
|
49
|
+
* credential itself), and the bytes are dropped as soon as it unmounts. So no
|
|
50
|
+
* reader is ever handed bytes another mount read — not even behind a host
|
|
51
|
+
* `getToken`, whose account the key cannot see — and every new reader passes
|
|
52
|
+
* the permission check again. While mounted the file is read once (a stored
|
|
53
|
+
* file does not change under its asset id); `refetch()` reads it again.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useAthenaAssetContent({ assetId, enabled, }: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error>;
|
|
56
|
+
/**
|
|
57
|
+
* An object URL for `blob` while it is shown, revoked when the blob changes
|
|
58
|
+
* or the component unmounts; `null` without a blob.
|
|
59
|
+
*/
|
|
60
|
+
export declare function useObjectUrl(blob: Blob | null | undefined): string | null;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useId, useState, useEffect } from "react";
|
|
3
|
+
import { getAthenaApiBaseUrl } from "../collab/mint.js";
|
|
4
|
+
import { AthenaSdkError, isAthenaSdkError } from "../diagnostics/errors.js";
|
|
5
|
+
import { useAthenaQueryIdentity, useAthenaQueryClient } from "../lib/use-athena-rest-query.js";
|
|
6
|
+
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
7
|
+
import { sendWithCredentialRenewal } from "../runtime/credential-renewal.js";
|
|
8
|
+
function athenaAssetContentPath(assetId) {
|
|
9
|
+
return `/api/asset/content/${encodeURIComponent(assetId)}`;
|
|
10
|
+
}
|
|
11
|
+
function isAbortError(error) {
|
|
12
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
13
|
+
}
|
|
14
|
+
function assetContentError({
|
|
15
|
+
error,
|
|
16
|
+
assetId,
|
|
17
|
+
message,
|
|
18
|
+
hint
|
|
19
|
+
}) {
|
|
20
|
+
if (isAbortError(error) || isAthenaSdkError(error)) return error;
|
|
21
|
+
return new AthenaSdkError({
|
|
22
|
+
code: "asset_content_failed",
|
|
23
|
+
message,
|
|
24
|
+
cause: error,
|
|
25
|
+
hint,
|
|
26
|
+
context: { assetId }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function fetchAthenaAssetContent({
|
|
30
|
+
backendUrl,
|
|
31
|
+
auth,
|
|
32
|
+
assetId,
|
|
33
|
+
signal
|
|
34
|
+
}) {
|
|
35
|
+
const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;
|
|
36
|
+
let response;
|
|
37
|
+
try {
|
|
38
|
+
response = await sendWithCredentialRenewal(
|
|
39
|
+
auth,
|
|
40
|
+
(headers) => fetch(url, { method: "GET", headers, signal: signal ?? null })
|
|
41
|
+
);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw assetContentError({
|
|
44
|
+
error,
|
|
45
|
+
assetId,
|
|
46
|
+
message: "Reading the asset could not reach the Agora API, or its credential could not be resolved.",
|
|
47
|
+
hint: "Check the network, that backendUrl points at the Agora API base, and that getToken resolves."
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const refused = response.status === 401 || response.status === 403;
|
|
52
|
+
throw new AthenaSdkError({
|
|
53
|
+
code: refused ? "auth_rejected" : "asset_content_failed",
|
|
54
|
+
message: `Reading the asset failed: ${response.status}`,
|
|
55
|
+
status: response.status,
|
|
56
|
+
detail: await response.json().catch(() => void 0),
|
|
57
|
+
hint: refused ? "The signed-in user cannot read this asset." : response.status === 404 ? "The asset has no stored file, or it no longer exists." : "Check that backendUrl points at the Agora API base.",
|
|
58
|
+
context: { assetId }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return await response.blob();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw assetContentError({
|
|
65
|
+
error,
|
|
66
|
+
assetId,
|
|
67
|
+
message: "Reading the asset failed while its file downloaded.",
|
|
68
|
+
hint: "Check the network and try again."
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const NO_RETRY_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 409]);
|
|
73
|
+
function retryTransient(failureCount, error) {
|
|
74
|
+
const status = error.status;
|
|
75
|
+
if (typeof status === "number" && NO_RETRY_STATUSES.has(status)) return false;
|
|
76
|
+
return failureCount < 1;
|
|
77
|
+
}
|
|
78
|
+
function useAthenaAssetContent({
|
|
79
|
+
assetId,
|
|
80
|
+
enabled = true
|
|
81
|
+
}) {
|
|
82
|
+
const config = useAthenaConfig();
|
|
83
|
+
const { backendUrl, apiKey, getToken } = config;
|
|
84
|
+
const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });
|
|
85
|
+
const queryClient = useAthenaQueryClient();
|
|
86
|
+
const reader = useId();
|
|
87
|
+
return useQuery(
|
|
88
|
+
{
|
|
89
|
+
queryKey: ["athena-sdk", "asset-content", backendUrl, ...identity, assetId, reader],
|
|
90
|
+
// `config.token` is read at fetch time: the provider serves the latest
|
|
91
|
+
// token through a getter, so rotations reach the request.
|
|
92
|
+
queryFn: ({ signal }) => fetchAthenaAssetContent({
|
|
93
|
+
backendUrl,
|
|
94
|
+
auth: { apiKey, token: config.token, getToken },
|
|
95
|
+
assetId: assetId ?? "",
|
|
96
|
+
signal
|
|
97
|
+
}),
|
|
98
|
+
enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,
|
|
99
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
100
|
+
gcTime: 0,
|
|
101
|
+
refetchOnWindowFocus: false,
|
|
102
|
+
refetchOnReconnect: false,
|
|
103
|
+
retry: retryTransient
|
|
104
|
+
},
|
|
105
|
+
queryClient
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function useObjectUrl(blob) {
|
|
109
|
+
const [url, setUrl] = useState(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!blob) {
|
|
112
|
+
setUrl(null);
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
116
|
+
setUrl(objectUrl);
|
|
117
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
118
|
+
}, [blob]);
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
athenaAssetContentPath,
|
|
123
|
+
fetchAthenaAssetContent,
|
|
124
|
+
useAthenaAssetContent,
|
|
125
|
+
useObjectUrl
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=use-asset-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-content.js","sources":["../../src/assets/use-asset-content.ts"],"sourcesContent":["/**\n * An asset's stored file, read with the signed-in user's own credential:\n * `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`\n * `serve_asset_content`), which passes the asset's permission check\n * (`get_asset_with_permission_check`) and streams the bytes through Agora.\n * No link is minted — no presigned storage URL, no embed token — so nothing\n * that could be opened without the credential ever exists: the bytes land in\n * a `Blob` that only this page holds, and a media element plays it through an\n * object URL (`useObjectUrl`).\n *\n * This is how the SDK plays an asset's video: a `<video src>` cannot carry\n * the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the\n * alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)\n * each hand out a URL that works for whoever holds it. The whole file is read\n * before playback starts, so read it when the person asks to play, not on\n * mount.\n */\nimport { useQuery, type UseQueryResult } from '@tanstack/react-query';\nimport { useEffect, useId, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError, isAthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaQueryClient, useAthenaQueryIdentity } from '../lib/use-athena-rest-query';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\n\n/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */\nexport function athenaAssetContentPath(assetId: string): string {\n return `/api/asset/content/${encodeURIComponent(assetId)}`;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/** `error` as an `asset_content_failed` SDK error; an abort or an SDK error passes through. */\nfunction assetContentError({\n error,\n assetId,\n message,\n hint,\n}: {\n error: unknown;\n assetId: string;\n message: string;\n hint: string;\n}): unknown {\n if (isAbortError(error) || isAthenaSdkError(error)) return error;\n return new AthenaSdkError({\n code: 'asset_content_failed',\n message,\n cause: error,\n hint,\n context: { assetId },\n });\n}\n\n/**\n * Read an asset's stored file as a `Blob`. The credential rides\n * `Authorization: Bearer` (resolved live through `getToken` when the host gave\n * one) or `X-API-KEY`; a refused bearer (`401`) is renewed once\n * (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —\n * `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP\n * `status` when the server answered), a credential that could not be\n * resolved and a download cut off mid-body included; only an abort keeps its\n * own identity.\n */\nexport async function fetchAthenaAssetContent({\n backendUrl,\n auth,\n assetId,\n signal,\n}: {\n backendUrl: string;\n auth: StatewireAuthSource;\n assetId: string;\n signal?: AbortSignal;\n}): Promise<Blob> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;\n let response: Response;\n try {\n response = await sendWithCredentialRenewal(auth, (headers) =>\n fetch(url, { method: 'GET', headers, signal: signal ?? null }),\n );\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset could not reach the Agora API, or its credential could not be resolved.',\n hint: 'Check the network, that backendUrl points at the Agora API base, and that getToken resolves.',\n });\n }\n if (!response.ok) {\n const refused = response.status === 401 || response.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'asset_content_failed',\n message: `Reading the asset failed: ${response.status}`,\n status: response.status,\n detail: await response.json().catch(() => undefined),\n hint: refused\n ? 'The signed-in user cannot read this asset.'\n : response.status === 404\n ? 'The asset has no stored file, or it no longer exists.'\n : 'Check that backendUrl points at the Agora API base.',\n context: { assetId },\n });\n }\n try {\n return await response.blob();\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset failed while its file downloaded.',\n hint: 'Check the network and try again.',\n });\n }\n}\n\nconst NO_RETRY_STATUSES: ReadonlySet<number> = new Set([400, 401, 403, 404, 409]);\n\n/** Retry a transient failure once; a refusal or a missing file answers the same way again. */\nfunction retryTransient(failureCount: number, error: Error): boolean {\n const status = (error as { status?: unknown }).status;\n if (typeof status === 'number' && NO_RETRY_STATUSES.has(status)) return false;\n return failureCount < 1;\n}\n\nexport interface UseAthenaAssetContentOptions {\n /** The asset to read; `null` holds the query idle. */\n assetId: string | null;\n /** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * An asset's stored file through TanStack Query, with the provider's\n * `backendUrl` and credentials. Each mounted hook reads its own copy: the key\n * carries the hook instance as well as the asset, the signed-in principal and\n * a fingerprint of the credential (`useAthenaQueryIdentity`, never the\n * credential itself), and the bytes are dropped as soon as it unmounts. So no\n * reader is ever handed bytes another mount read — not even behind a host\n * `getToken`, whose account the key cannot see — and every new reader passes\n * the permission check again. While mounted the file is read once (a stored\n * file does not change under its asset id); `refetch()` reads it again.\n */\nexport function useAthenaAssetContent({\n assetId,\n enabled = true,\n}: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });\n const queryClient = useAthenaQueryClient();\n const reader = useId();\n return useQuery(\n {\n queryKey: ['athena-sdk', 'asset-content', backendUrl, ...identity, assetId, reader],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchAthenaAssetContent({\n backendUrl,\n auth: { apiKey, token: config.token, getToken },\n assetId: assetId ?? '',\n signal,\n }),\n enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,\n staleTime: Number.POSITIVE_INFINITY,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: retryTransient,\n },\n queryClient,\n );\n}\n\n/**\n * An object URL for `blob` while it is shown, revoked when the blob changes\n * or the component unmounts; `null` without a blob.\n */\nexport function useObjectUrl(blob: Blob | null | undefined): string | null {\n const [url, setUrl] = useState<string | null>(null);\n useEffect(() => {\n if (!blob) {\n setUrl(null);\n return undefined;\n }\n const objectUrl = URL.createObjectURL(blob);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [blob]);\n return url;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -28,6 +28,10 @@ const DEFAULT_TOOLKIT_THEME = { color: "#919EAB" };
|
|
|
28
28
|
const DEFAULT_GROUPING = groupingFunction.createToolBasedGroupingFunction(toolkitMap.defaultGetToolGroupKey);
|
|
29
29
|
const SuperGroupingContext = react.createContext(null);
|
|
30
30
|
const SuperGroupingProvider = SuperGroupingContext.Provider;
|
|
31
|
+
const GroupToolUIsMountedContext = react.createContext(true);
|
|
32
|
+
function GroupToolUI({ children }) {
|
|
33
|
+
return react.useContext(GroupToolUIsMountedContext) ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children }) : null;
|
|
34
|
+
}
|
|
31
35
|
const CollapsibleGroup = ({
|
|
32
36
|
groupKey,
|
|
33
37
|
indices,
|
|
@@ -35,21 +39,21 @@ const CollapsibleGroup = ({
|
|
|
35
39
|
themes,
|
|
36
40
|
forceOpen = false,
|
|
37
41
|
compactWhileRunning = false,
|
|
42
|
+
mountHidden = false,
|
|
38
43
|
headerActions,
|
|
39
44
|
groupingFunction: groupingFunction2,
|
|
40
45
|
className
|
|
41
46
|
}) => {
|
|
42
|
-
var _a;
|
|
43
47
|
const panelId = react.useId();
|
|
44
48
|
const isMessageRunning = react$1.useAuiState((s) => {
|
|
45
|
-
var
|
|
46
|
-
return ((
|
|
49
|
+
var _a;
|
|
50
|
+
return ((_a = s.message.status) == null ? void 0 : _a.type) === "running";
|
|
47
51
|
});
|
|
48
52
|
const messageContent = react$1.useAuiState(
|
|
49
53
|
(s) => s.message.role === "assistant" ? s.message.content : void 0
|
|
50
54
|
);
|
|
51
|
-
const
|
|
52
|
-
const grouping = groupingFunction2 ??
|
|
55
|
+
const superGrouping = react.useContext(SuperGroupingContext);
|
|
56
|
+
const grouping = groupingFunction2 ?? (superGrouping == null ? void 0 : superGrouping.groupingFunction) ?? DEFAULT_GROUPING;
|
|
53
57
|
const lastIndex = indices.at(-1);
|
|
54
58
|
const isLastGroup = react.useMemo(() => {
|
|
55
59
|
if (!messageContent) return false;
|
|
@@ -59,8 +63,8 @@ const CollapsibleGroup = ({
|
|
|
59
63
|
const toolCallCount = react.useMemo(
|
|
60
64
|
() => messageContent ? indices.reduce(
|
|
61
65
|
(count, index) => {
|
|
62
|
-
var
|
|
63
|
-
return ((
|
|
66
|
+
var _a;
|
|
67
|
+
return ((_a = messageContent[index]) == null ? void 0 : _a.type) === "tool-call" ? count + 1 : count;
|
|
64
68
|
},
|
|
65
69
|
0
|
|
66
70
|
) : 0,
|
|
@@ -73,6 +77,9 @@ const CollapsibleGroup = ({
|
|
|
73
77
|
const [userOpened, setOpen] = react.useState();
|
|
74
78
|
const autoOpen = isLastGroup && isMessageRunning && !compactWhileRunning;
|
|
75
79
|
const open = userOpened ?? (autoOpen || forceOpen);
|
|
80
|
+
const mountToolUIs = open || isMessageRunning || mountHidden || (superGrouping == null ? void 0 : superGrouping.mountHidden) === true;
|
|
81
|
+
const [toolUIsMounted, setToolUIsMounted] = react.useState(mountToolUIs);
|
|
82
|
+
if (mountToolUIs && !toolUIsMounted) setToolUIsMounted(true);
|
|
76
83
|
if (!groupKey || indices.length <= 1) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
77
84
|
const toggle = () => setOpen(!open);
|
|
78
85
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -144,7 +151,7 @@ const CollapsibleGroup = ({
|
|
|
144
151
|
open ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
|
145
152
|
),
|
|
146
153
|
style: { borderColor: `${theme.color}33` },
|
|
147
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2 px-5 py-4", children }) })
|
|
154
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(GroupToolUIsMountedContext.Provider, { value: toolUIsMounted, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2 px-5 py-4", children }) }) })
|
|
148
155
|
}
|
|
149
156
|
)
|
|
150
157
|
]
|
|
@@ -153,6 +160,7 @@ const CollapsibleGroup = ({
|
|
|
153
160
|
};
|
|
154
161
|
exports.CollapsibleGroup = CollapsibleGroup;
|
|
155
162
|
exports.DEFAULT_TOOLKIT_THEME = DEFAULT_TOOLKIT_THEME;
|
|
163
|
+
exports.GroupToolUI = GroupToolUI;
|
|
156
164
|
exports.SuperGroupingProvider = SuperGroupingProvider;
|
|
157
165
|
exports.TOOLKIT_THEMES = TOOLKIT_THEMES;
|
|
158
166
|
//# sourceMappingURL=CollapsibleGroup.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollapsibleGroup.cjs","sources":["../../../src/chat/super-grouping/CollapsibleGroup.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useId, useMemo, useState } from 'react';\nimport { cn } from '../../lib/utils';\nimport { createToolBasedGroupingFunction, type GroupingFunction } from './grouping-function';\nimport { defaultGetToolGroupKey } from './toolkit-map';\n\n/**\n * Per-toolkit theme: the brand color that tints the group header, and an\n * optional icon URL a host serves (Olympus ships toolkit SVGs; the SDK does\n * not bundle images, so it tints only).\n */\nexport type ToolkitTheme = {\n /** Hex brand color for this toolkit. */\n color: string;\n /** Absolute or root-relative URL of the toolkit icon, when the host has one. */\n icon?: string;\n};\n\n/** Brand colors keyed by toolkit display name — the group keys the grouping function emits. */\nexport const TOOLKIT_THEMES: Record<string, ToolkitTheme> = {\n Web: { color: '#7BCCFA' },\n Document: { color: '#4586F9' },\n 'Document (Word)': { color: '#4586F9' },\n Notebook: { color: '#FFAB00' },\n Python: { color: '#00A76F' },\n Spreadsheet: { color: '#00A76F' },\n Email: { color: '#B584FF' },\n AOP: { color: '#336DFF' },\n App: { color: '#7336F5' },\n Presentation: { color: '#F56B36' },\n 'User Interface': { color: '#7336F5' },\n Canvas: { color: '#002542' },\n Computer: { color: '#3E0042' },\n Database: { color: '#7336F5' },\n 'Chrome Browser': { color: '#4285F4' },\n};\n\nexport const DEFAULT_TOOLKIT_THEME: ToolkitTheme = { color: '#919EAB' };\n\nconst DEFAULT_GROUPING = createToolBasedGroupingFunction(defaultGetToolGroupKey);\n\n/**\n * The grouping in force for the message being rendered. `SuperGroupingCard`\n * provides it so each `CollapsibleGroup` can tell whether it is the last\n * grouped block (the one that opens by default while the run is live) without\n * being handed the function as a prop through `Unstable_PartsGrouped`.\n */\nconst SuperGroupingContext = createContext<{ groupingFunction: GroupingFunction } | null>(null);\n\nexport const SuperGroupingProvider = SuperGroupingContext.Provider;\n\nexport interface CollapsibleGroupProps {\n /** Group key from the grouping function (toolkit display name); `undefined` for ungrouped parts. */\n groupKey: string | undefined;\n /** Indices of the parts belonging to this group. */\n indices: number[];\n children?: ReactNode;\n /** Host toolkit themes (colors, icons), merged over the built-in colors. */\n themes?: Record<string, ToolkitTheme>;\n /**\n * Keep the group open regardless of the run state — a host rule such as\n * \"an unsubmitted email draft stays visible\". The user's own toggle still\n * wins once used.\n */\n forceOpen?: boolean;\n /**\n * Do not open the live group by default (screenshot-heavy browser-control\n * runs hold hundreds of actions); the user can still expand it.\n */\n compactWhileRunning?: boolean;\n /** Extra header controls, rendered as siblings of the toggle (never nested buttons). */\n headerActions?: ReactNode;\n /** The grouping used to find the last grouped block; defaults to the card's, then the SDK's. */\n groupingFunction?: GroupingFunction;\n className?: string;\n}\n\n/**\n * Per-toolkit collapsible block inside the grouped details panel — \"Using\n * Spreadsheet Toolkit · 4 tool calls\" — used by the SDK, Olympus, and the\n * Chrome extension. The last grouped block opens by default while the message\n * is running and settles closed; the user's toggle is remembered for the\n * mounted block.\n *\n * Selector discipline: one raw `s.message.content` read; every per-group\n * derivation (grouping, counts) runs in `useMemo` off it, so the grouping\n * pass happens once per content change instead of once per selector pass.\n */\nexport const CollapsibleGroup = ({\n groupKey,\n indices,\n children,\n themes,\n forceOpen = false,\n compactWhileRunning = false,\n headerActions,\n groupingFunction,\n className,\n}: CollapsibleGroupProps) => {\n const panelId = useId();\n const isMessageRunning = useAuiState((s) => s.message.status?.type === 'running');\n const messageContent = useAuiState((s) =>\n s.message.role === 'assistant' ? s.message.content : undefined,\n );\n const contextGrouping = useContext(SuperGroupingContext)?.groupingFunction;\n const grouping = groupingFunction ?? contextGrouping ?? DEFAULT_GROUPING;\n\n const lastIndex = indices.at(-1);\n const isLastGroup = useMemo(() => {\n if (!messageContent) return false;\n const lastGroup = grouping(messageContent)\n .filter((group) => group.indices.length > 1)\n .at(-1);\n return lastGroup?.indices.at(-1) === lastIndex;\n }, [messageContent, grouping, lastIndex]);\n\n const toolCallCount = useMemo(\n () =>\n messageContent\n ? indices.reduce(\n (count, index) => (messageContent[index]?.type === 'tool-call' ? count + 1 : count),\n 0,\n )\n : 0,\n [messageContent, indices],\n );\n\n const theme = useMemo<ToolkitTheme>(\n () => (groupKey ? { ...TOOLKIT_THEMES, ...themes }[groupKey] ?? DEFAULT_TOOLKIT_THEME : DEFAULT_TOOLKIT_THEME),\n [groupKey, themes],\n );\n\n const [userOpened, setOpen] = useState<boolean>();\n const autoOpen = isLastGroup && isMessageRunning && !compactWhileRunning;\n const open = userOpened ?? (autoOpen || forceOpen);\n\n if (!groupKey || indices.length <= 1) return <>{children}</>;\n\n const toggle = () => setOpen(!open);\n\n return (\n <div\n data-slot=\"collapsible-group\"\n data-group-key={groupKey}\n className={cn('my-4 rounded-2xl border shadow-sm', className)}\n style={{\n borderColor: `${theme.color}33`,\n background: `linear-gradient(to right, ${theme.color}0d, transparent, ${theme.color}0d)`,\n }}\n >\n <div className=\"flex w-full items-center rounded-2xl\">\n <button\n type=\"button\"\n onClick={toggle}\n aria-expanded={open}\n aria-controls={panelId}\n className=\"flex min-w-0 flex-1 items-center justify-between gap-3 rounded-2xl px-5 py-3 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/5\"\n >\n <span className=\"flex min-w-0 items-center gap-3 text-foreground/90\">\n {theme.icon ? (\n <img src={theme.icon} alt=\"\" aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n ) : (\n <span\n className=\"inline-block size-2 shrink-0 rounded-full\"\n style={{ backgroundColor: theme.color }}\n aria-hidden=\"true\"\n />\n )}\n <span className=\"truncate text-sm font-semibold\">\n {isMessageRunning ? 'Using ' : 'Used '}\n {groupKey} Toolkit\n </span>\n </span>\n <span className=\"shrink-0 text-xs font-medium text-muted-foreground\">\n {toolCallCount} tool {toolCallCount === 1 ? 'call' : 'calls'}\n </span>\n </button>\n <div className=\"flex shrink-0 items-center gap-1 pr-3 pl-1\">\n {headerActions}\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={open ? 'Collapse toolkit group' : 'Expand toolkit group'}\n aria-expanded={open}\n aria-controls={panelId}\n className=\"flex items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/5\"\n >\n {open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}\n </button>\n </div>\n </div>\n\n <div\n id={panelId}\n className={cn(\n 'grid overflow-hidden border-t transition-[grid-template-rows] duration-200 ease-in-out motion-reduce:transition-none',\n open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',\n )}\n style={{ borderColor: `${theme.color}33` }}\n >\n <div className=\"overflow-hidden\">\n <div className=\"flex flex-col gap-2 px-5 py-4\">{children}</div>\n </div>\n </div>\n </div>\n );\n};\n"],"names":["createToolBasedGroupingFunction","defaultGetToolGroupKey","createContext","groupingFunction","useId","useAuiState","_a","useContext","useMemo","useState","jsxs","cn","jsx","ChevronUp","ChevronDown"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CollapsibleGroup.cjs","sources":["../../../src/chat/super-grouping/CollapsibleGroup.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useId, useMemo, useState } from 'react';\nimport { cn } from '../../lib/utils';\nimport { createToolBasedGroupingFunction, type GroupingFunction } from './grouping-function';\nimport { defaultGetToolGroupKey } from './toolkit-map';\n\n/**\n * Per-toolkit theme: the brand color that tints the group header, and an\n * optional icon URL a host serves (Olympus ships toolkit SVGs; the SDK does\n * not bundle images, so it tints only).\n */\nexport type ToolkitTheme = {\n /** Hex brand color for this toolkit. */\n color: string;\n /** Absolute or root-relative URL of the toolkit icon, when the host has one. */\n icon?: string;\n};\n\n/** Brand colors keyed by toolkit display name — the group keys the grouping function emits. */\nexport const TOOLKIT_THEMES: Record<string, ToolkitTheme> = {\n Web: { color: '#7BCCFA' },\n Document: { color: '#4586F9' },\n 'Document (Word)': { color: '#4586F9' },\n Notebook: { color: '#FFAB00' },\n Python: { color: '#00A76F' },\n Spreadsheet: { color: '#00A76F' },\n Email: { color: '#B584FF' },\n AOP: { color: '#336DFF' },\n App: { color: '#7336F5' },\n Presentation: { color: '#F56B36' },\n 'User Interface': { color: '#7336F5' },\n Canvas: { color: '#002542' },\n Computer: { color: '#3E0042' },\n Database: { color: '#7336F5' },\n 'Chrome Browser': { color: '#4285F4' },\n};\n\nexport const DEFAULT_TOOLKIT_THEME: ToolkitTheme = { color: '#919EAB' };\n\nconst DEFAULT_GROUPING = createToolBasedGroupingFunction(defaultGetToolGroupKey);\n\n/**\n * The grouping in force for the message being rendered. `SuperGroupingCard`\n * provides it so each `CollapsibleGroup` can tell whether it is the last\n * grouped block (the one that opens by default while the run is live) without\n * being handed the function as a prop through `GroupedMessageParts`, and\n * whether the card keeps its tool UIs mounted while hidden (`mountHidden`:\n * its turn is active, or a deep link targets it).\n */\nconst SuperGroupingContext = createContext<{\n groupingFunction: GroupingFunction;\n mountHidden?: boolean;\n} | null>(null);\n\nexport const SuperGroupingProvider = SuperGroupingContext.Provider;\n\n/** Whether the group a tool UI renders in has mounted its tool UIs; outside a group, always. */\nconst GroupToolUIsMountedContext = createContext(true);\n\n/**\n * A tool call's UI inside a `CollapsibleGroup`: rendered once the group first\n * needs its tool UIs (see `CollapsibleGroup`). `GroupedMessageParts` renders\n * every tool call through it.\n */\nexport function GroupToolUI({ children }: { children: ReactNode }) {\n return useContext(GroupToolUIsMountedContext) ? <>{children}</> : null;\n}\n\nexport interface CollapsibleGroupProps {\n /** Group key from the grouping function (toolkit display name); `undefined` for ungrouped parts. */\n groupKey: string | undefined;\n /** Indices of the parts belonging to this group. */\n indices: number[];\n children?: ReactNode;\n /** Host toolkit themes (colors, icons), merged over the built-in colors. */\n themes?: Record<string, ToolkitTheme>;\n /**\n * Keep the group open regardless of the run state — a host rule such as\n * \"an unsubmitted email draft stays visible\". The user's own toggle still\n * wins once used.\n */\n forceOpen?: boolean;\n /**\n * Do not open the live group by default (screenshot-heavy browser-control\n * runs hold hundreds of actions); the user can still expand it.\n */\n compactWhileRunning?: boolean;\n /**\n * Mount the tool UIs while collapsed (hidden), so a host that needs to find\n * one — a deep link into one of its tool calls — can. The card a group\n * renders in asks the same while its turn is active or its `mountDetails` is\n * set.\n */\n mountHidden?: boolean;\n /** Extra header controls, rendered as siblings of the toggle (never nested buttons). */\n headerActions?: ReactNode;\n /** The grouping used to find the last grouped block; defaults to the card's, then the SDK's. */\n groupingFunction?: GroupingFunction;\n className?: string;\n}\n\n/**\n * Per-toolkit collapsible block inside the grouped details panel — \"Using\n * Spreadsheet Toolkit · 4 tool calls\" — used by the SDK, Olympus, and the\n * Chrome extension. The last grouped block opens by default while the message\n * is running and settles closed; the user's toggle is remembered for the\n * mounted block.\n *\n * The group's tool UIs (the tool calls `GroupedMessageParts` renders through\n * `GroupToolUI`) mount the first time they are needed — the group opens, its\n * turn is running (live tool UIs run their effects), or a host asks\n * (`mountHidden`) — and stay mounted after. A finished turn's collapsed groups\n * mount none of their tool UIs, which a long run holds hundreds of. The rest\n * of the body renders from the start: the agent's text between the calls is\n * part of the message that selections, in-page find and links into the\n * message count.\n *\n * Selector discipline: one raw `s.message.content` read; every per-group\n * derivation (grouping, counts) runs in `useMemo` off it, so the grouping\n * pass happens once per content change instead of once per selector pass.\n */\nexport const CollapsibleGroup = ({\n groupKey,\n indices,\n children,\n themes,\n forceOpen = false,\n compactWhileRunning = false,\n mountHidden = false,\n headerActions,\n groupingFunction,\n className,\n}: CollapsibleGroupProps) => {\n const panelId = useId();\n const isMessageRunning = useAuiState((s) => s.message.status?.type === 'running');\n const messageContent = useAuiState((s) =>\n s.message.role === 'assistant' ? s.message.content : undefined,\n );\n const superGrouping = useContext(SuperGroupingContext);\n const grouping = groupingFunction ?? superGrouping?.groupingFunction ?? DEFAULT_GROUPING;\n\n const lastIndex = indices.at(-1);\n const isLastGroup = useMemo(() => {\n if (!messageContent) return false;\n const lastGroup = grouping(messageContent)\n .filter((group) => group.indices.length > 1)\n .at(-1);\n return lastGroup?.indices.at(-1) === lastIndex;\n }, [messageContent, grouping, lastIndex]);\n\n const toolCallCount = useMemo(\n () =>\n messageContent\n ? indices.reduce(\n (count, index) => (messageContent[index]?.type === 'tool-call' ? count + 1 : count),\n 0,\n )\n : 0,\n [messageContent, indices],\n );\n\n const theme = useMemo<ToolkitTheme>(\n () => (groupKey ? { ...TOOLKIT_THEMES, ...themes }[groupKey] ?? DEFAULT_TOOLKIT_THEME : DEFAULT_TOOLKIT_THEME),\n [groupKey, themes],\n );\n\n const [userOpened, setOpen] = useState<boolean>();\n const autoOpen = isLastGroup && isMessageRunning && !compactWhileRunning;\n const open = userOpened ?? (autoOpen || forceOpen);\n\n const mountToolUIs =\n open || isMessageRunning || mountHidden || superGrouping?.mountHidden === true;\n const [toolUIsMounted, setToolUIsMounted] = useState(mountToolUIs);\n // Adjusting state during render is React's sanctioned derived-state pattern.\n if (mountToolUIs && !toolUIsMounted) setToolUIsMounted(true);\n\n if (!groupKey || indices.length <= 1) return <>{children}</>;\n\n const toggle = () => setOpen(!open);\n\n return (\n <div\n data-slot=\"collapsible-group\"\n data-group-key={groupKey}\n className={cn('my-4 rounded-2xl border shadow-sm', className)}\n style={{\n borderColor: `${theme.color}33`,\n background: `linear-gradient(to right, ${theme.color}0d, transparent, ${theme.color}0d)`,\n }}\n >\n <div className=\"flex w-full items-center rounded-2xl\">\n <button\n type=\"button\"\n onClick={toggle}\n aria-expanded={open}\n aria-controls={panelId}\n className=\"flex min-w-0 flex-1 items-center justify-between gap-3 rounded-2xl px-5 py-3 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/5\"\n >\n <span className=\"flex min-w-0 items-center gap-3 text-foreground/90\">\n {theme.icon ? (\n <img src={theme.icon} alt=\"\" aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n ) : (\n <span\n className=\"inline-block size-2 shrink-0 rounded-full\"\n style={{ backgroundColor: theme.color }}\n aria-hidden=\"true\"\n />\n )}\n <span className=\"truncate text-sm font-semibold\">\n {isMessageRunning ? 'Using ' : 'Used '}\n {groupKey} Toolkit\n </span>\n </span>\n <span className=\"shrink-0 text-xs font-medium text-muted-foreground\">\n {toolCallCount} tool {toolCallCount === 1 ? 'call' : 'calls'}\n </span>\n </button>\n <div className=\"flex shrink-0 items-center gap-1 pr-3 pl-1\">\n {headerActions}\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={open ? 'Collapse toolkit group' : 'Expand toolkit group'}\n aria-expanded={open}\n aria-controls={panelId}\n className=\"flex items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/5\"\n >\n {open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}\n </button>\n </div>\n </div>\n\n <div\n id={panelId}\n className={cn(\n 'grid overflow-hidden border-t transition-[grid-template-rows] duration-200 ease-in-out motion-reduce:transition-none',\n open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',\n )}\n style={{ borderColor: `${theme.color}33` }}\n >\n <div className=\"overflow-hidden\">\n <GroupToolUIsMountedContext.Provider value={toolUIsMounted}>\n <div className=\"flex flex-col gap-2 px-5 py-4\">{children}</div>\n </GroupToolUIsMountedContext.Provider>\n </div>\n </div>\n </div>\n );\n};\n"],"names":["createToolBasedGroupingFunction","defaultGetToolGroupKey","createContext","useContext","jsx","Fragment","groupingFunction","useId","useAuiState","useMemo","useState","jsxs","cn","ChevronUp","ChevronDown"],"mappings":"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"}
|
|
@@ -16,7 +16,16 @@ export declare const TOOLKIT_THEMES: Record<string, ToolkitTheme>;
|
|
|
16
16
|
export declare const DEFAULT_TOOLKIT_THEME: ToolkitTheme;
|
|
17
17
|
export declare const SuperGroupingProvider: import("react").Provider<{
|
|
18
18
|
groupingFunction: GroupingFunction;
|
|
19
|
+
mountHidden?: boolean;
|
|
19
20
|
} | null>;
|
|
21
|
+
/**
|
|
22
|
+
* A tool call's UI inside a `CollapsibleGroup`: rendered once the group first
|
|
23
|
+
* needs its tool UIs (see `CollapsibleGroup`). `GroupedMessageParts` renders
|
|
24
|
+
* every tool call through it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function GroupToolUI({ children }: {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}): import("react").JSX.Element | null;
|
|
20
29
|
export interface CollapsibleGroupProps {
|
|
21
30
|
/** Group key from the grouping function (toolkit display name); `undefined` for ungrouped parts. */
|
|
22
31
|
groupKey: string | undefined;
|
|
@@ -36,6 +45,13 @@ export interface CollapsibleGroupProps {
|
|
|
36
45
|
* runs hold hundreds of actions); the user can still expand it.
|
|
37
46
|
*/
|
|
38
47
|
compactWhileRunning?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Mount the tool UIs while collapsed (hidden), so a host that needs to find
|
|
50
|
+
* one — a deep link into one of its tool calls — can. The card a group
|
|
51
|
+
* renders in asks the same while its turn is active or its `mountDetails` is
|
|
52
|
+
* set.
|
|
53
|
+
*/
|
|
54
|
+
mountHidden?: boolean;
|
|
39
55
|
/** Extra header controls, rendered as siblings of the toggle (never nested buttons). */
|
|
40
56
|
headerActions?: ReactNode;
|
|
41
57
|
/** The grouping used to find the last grouped block; defaults to the card's, then the SDK's. */
|
|
@@ -49,8 +65,17 @@ export interface CollapsibleGroupProps {
|
|
|
49
65
|
* is running and settles closed; the user's toggle is remembered for the
|
|
50
66
|
* mounted block.
|
|
51
67
|
*
|
|
68
|
+
* The group's tool UIs (the tool calls `GroupedMessageParts` renders through
|
|
69
|
+
* `GroupToolUI`) mount the first time they are needed — the group opens, its
|
|
70
|
+
* turn is running (live tool UIs run their effects), or a host asks
|
|
71
|
+
* (`mountHidden`) — and stay mounted after. A finished turn's collapsed groups
|
|
72
|
+
* mount none of their tool UIs, which a long run holds hundreds of. The rest
|
|
73
|
+
* of the body renders from the start: the agent's text between the calls is
|
|
74
|
+
* part of the message that selections, in-page find and links into the
|
|
75
|
+
* message count.
|
|
76
|
+
*
|
|
52
77
|
* Selector discipline: one raw `s.message.content` read; every per-group
|
|
53
78
|
* derivation (grouping, counts) runs in `useMemo` off it, so the grouping
|
|
54
79
|
* pass happens once per content change instead of once per selector pass.
|
|
55
80
|
*/
|
|
56
|
-
export declare const CollapsibleGroup: ({ groupKey, indices, children, themes, forceOpen, compactWhileRunning, headerActions, groupingFunction, className, }: CollapsibleGroupProps) => import("react").JSX.Element;
|
|
81
|
+
export declare const CollapsibleGroup: ({ groupKey, indices, children, themes, forceOpen, compactWhileRunning, mountHidden, headerActions, groupingFunction, className, }: CollapsibleGroupProps) => import("react").JSX.Element;
|
|
@@ -26,6 +26,10 @@ const DEFAULT_TOOLKIT_THEME = { color: "#919EAB" };
|
|
|
26
26
|
const DEFAULT_GROUPING = createToolBasedGroupingFunction(defaultGetToolGroupKey);
|
|
27
27
|
const SuperGroupingContext = createContext(null);
|
|
28
28
|
const SuperGroupingProvider = SuperGroupingContext.Provider;
|
|
29
|
+
const GroupToolUIsMountedContext = createContext(true);
|
|
30
|
+
function GroupToolUI({ children }) {
|
|
31
|
+
return useContext(GroupToolUIsMountedContext) ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
|
|
32
|
+
}
|
|
29
33
|
const CollapsibleGroup = ({
|
|
30
34
|
groupKey,
|
|
31
35
|
indices,
|
|
@@ -33,21 +37,21 @@ const CollapsibleGroup = ({
|
|
|
33
37
|
themes,
|
|
34
38
|
forceOpen = false,
|
|
35
39
|
compactWhileRunning = false,
|
|
40
|
+
mountHidden = false,
|
|
36
41
|
headerActions,
|
|
37
42
|
groupingFunction,
|
|
38
43
|
className
|
|
39
44
|
}) => {
|
|
40
|
-
var _a;
|
|
41
45
|
const panelId = useId();
|
|
42
46
|
const isMessageRunning = useAuiState((s) => {
|
|
43
|
-
var
|
|
44
|
-
return ((
|
|
47
|
+
var _a;
|
|
48
|
+
return ((_a = s.message.status) == null ? void 0 : _a.type) === "running";
|
|
45
49
|
});
|
|
46
50
|
const messageContent = useAuiState(
|
|
47
51
|
(s) => s.message.role === "assistant" ? s.message.content : void 0
|
|
48
52
|
);
|
|
49
|
-
const
|
|
50
|
-
const grouping = groupingFunction ??
|
|
53
|
+
const superGrouping = useContext(SuperGroupingContext);
|
|
54
|
+
const grouping = groupingFunction ?? (superGrouping == null ? void 0 : superGrouping.groupingFunction) ?? DEFAULT_GROUPING;
|
|
51
55
|
const lastIndex = indices.at(-1);
|
|
52
56
|
const isLastGroup = useMemo(() => {
|
|
53
57
|
if (!messageContent) return false;
|
|
@@ -57,8 +61,8 @@ const CollapsibleGroup = ({
|
|
|
57
61
|
const toolCallCount = useMemo(
|
|
58
62
|
() => messageContent ? indices.reduce(
|
|
59
63
|
(count, index) => {
|
|
60
|
-
var
|
|
61
|
-
return ((
|
|
64
|
+
var _a;
|
|
65
|
+
return ((_a = messageContent[index]) == null ? void 0 : _a.type) === "tool-call" ? count + 1 : count;
|
|
62
66
|
},
|
|
63
67
|
0
|
|
64
68
|
) : 0,
|
|
@@ -71,6 +75,9 @@ const CollapsibleGroup = ({
|
|
|
71
75
|
const [userOpened, setOpen] = useState();
|
|
72
76
|
const autoOpen = isLastGroup && isMessageRunning && !compactWhileRunning;
|
|
73
77
|
const open = userOpened ?? (autoOpen || forceOpen);
|
|
78
|
+
const mountToolUIs = open || isMessageRunning || mountHidden || (superGrouping == null ? void 0 : superGrouping.mountHidden) === true;
|
|
79
|
+
const [toolUIsMounted, setToolUIsMounted] = useState(mountToolUIs);
|
|
80
|
+
if (mountToolUIs && !toolUIsMounted) setToolUIsMounted(true);
|
|
74
81
|
if (!groupKey || indices.length <= 1) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
75
82
|
const toggle = () => setOpen(!open);
|
|
76
83
|
return /* @__PURE__ */ jsxs(
|
|
@@ -142,7 +149,7 @@ const CollapsibleGroup = ({
|
|
|
142
149
|
open ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
|
143
150
|
),
|
|
144
151
|
style: { borderColor: `${theme.color}33` },
|
|
145
|
-
children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-5 py-4", children }) })
|
|
152
|
+
children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: /* @__PURE__ */ jsx(GroupToolUIsMountedContext.Provider, { value: toolUIsMounted, children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-5 py-4", children }) }) })
|
|
146
153
|
}
|
|
147
154
|
)
|
|
148
155
|
]
|
|
@@ -152,6 +159,7 @@ const CollapsibleGroup = ({
|
|
|
152
159
|
export {
|
|
153
160
|
CollapsibleGroup,
|
|
154
161
|
DEFAULT_TOOLKIT_THEME,
|
|
162
|
+
GroupToolUI,
|
|
155
163
|
SuperGroupingProvider,
|
|
156
164
|
TOOLKIT_THEMES
|
|
157
165
|
};
|