@gooddata/sdk-ui-pluggable-host 11.45.0-alpha.0 → 11.45.0-alpha.2
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/esm/components/HostUiContainer.d.ts.map +1 -1
- package/esm/components/HostUiContainer.js +54 -7
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/loader/legacyRedirect.d.ts +17 -0
- package/esm/loader/legacyRedirect.d.ts.map +1 -0
- package/esm/loader/legacyRedirect.js +51 -0
- package/esm/platformContext/backend.d.ts.map +1 -1
- package/esm/platformContext/backend.js +20 -1
- package/esm/registry/pluggableApplicationsRegistry.d.ts.map +1 -1
- package/esm/registry/pluggableApplicationsRegistry.js +14 -1
- package/esm/sdk-ui-pluggable-host.d.ts +18 -0
- package/esm/ui/DefaultHostUi.js +16 -43
- package/esm/ui/GenAIChat.d.ts +18 -1
- package/esm/ui/GenAIChat.d.ts.map +1 -1
- package/esm/ui/GenAIChat.js +69 -97
- package/esm/ui/HostChat.d.ts +64 -0
- package/esm/ui/HostChat.d.ts.map +1 -0
- package/esm/ui/HostChat.js +75 -0
- package/esm/ui/HostChrome.d.ts +12 -24
- package/esm/ui/HostChrome.d.ts.map +1 -1
- package/esm/ui/HostChrome.js +11 -52
- package/esm/ui/PluggableApplicationRenderer.d.ts +24 -2
- package/esm/ui/PluggableApplicationRenderer.d.ts.map +1 -1
- package/esm/ui/PluggableApplicationRenderer.js +31 -13
- package/esm/ui/useHostChromeChat.d.ts +12 -1
- package/esm/ui/useHostChromeChat.d.ts.map +1 -1
- package/esm/ui/useHostChromeChat.js +2 -2
- package/package.json +17 -17
package/esm/ui/GenAIChat.js
CHANGED
|
@@ -1,108 +1,80 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// (C) 2026 GoodData Corporation
|
|
3
|
-
import { useCallback
|
|
3
|
+
import { useCallback } from "react";
|
|
4
4
|
import { defineMessage, useIntl } from "react-intl";
|
|
5
5
|
import { useBackendStrict } from "@gooddata/sdk-ui";
|
|
6
|
-
import {
|
|
7
|
-
import { GenAIChatDialog, clearThreadAction, makeTextContents, makeUserMessage, newMessageAction, setUserContextAction, } from "@gooddata/sdk-ui-gen-ai/internal";
|
|
6
|
+
import { GenAIChatDialogConnected } from "@gooddata/sdk-ui-gen-ai/internal";
|
|
8
7
|
import { HEADER_CHAT_BUTTON_ID, useToastMessage } from "@gooddata/sdk-ui-kit";
|
|
9
|
-
|
|
8
|
+
// DOM id of the embedded dashboard's AI trigger button (defined in gdc-dashboards-runtime as
|
|
9
|
+
// EMBED_AI_TRIGGER_ID). Used as the chat's focus-return target when running embedded, where the host
|
|
10
|
+
// header chat button does not exist. Kept as a literal to avoid a host→app package dependency.
|
|
11
|
+
const EMBEDDED_AI_TRIGGER_ID = "embed-ai-trigger";
|
|
12
|
+
const EMBEDDED_CHAT_CLASS = "gd-gen-ai-chat-embedded";
|
|
13
|
+
/**
|
|
14
|
+
* Host chrome's chat adapter. The generic chat wiring (object types, the seeded-question dispatch flow,
|
|
15
|
+
* default link handling, event normalization) lives in `GenAIChatDialogConnected`
|
|
16
|
+
* (`@gooddata/sdk-ui-gen-ai/internal`); this adapter only supplies the host's data sources, renders the
|
|
17
|
+
* host's own save-visualization / copy toasts, and forwards telemetry events through `onEvent`.
|
|
18
|
+
*/
|
|
19
|
+
export function GenAIChat({ workspaceId, open, onOpen, onClose, askedQuestion, askSeq, userContext, includeTags, excludeTags, canManageProject, canAnalyzeProject, canFullControl, settings, dialogPosition, embedded, onAppLinkClick, onEvent, }) {
|
|
10
20
|
const { addSuccess, addError } = useToastMessage();
|
|
11
21
|
const intl = useIntl();
|
|
12
22
|
const backend = useBackendStrict();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// Embedded link handling (this handler is only wired when `embedded`). Delegate to the active app —
|
|
24
|
+
// it opens visualization links as an in-place overlay — and never let a link navigate the embedded
|
|
25
|
+
// iframe away, matching the previous embedded dashboard chat which prevented default for every link
|
|
26
|
+
// type (only visualizations did anything). Non-embedded mode uses the connected wrapper's default
|
|
27
|
+
// open/navigate handler instead.
|
|
28
|
+
const onLinkClick = useCallback((link) => {
|
|
29
|
+
onAppLinkClick?.({
|
|
30
|
+
type: link.type,
|
|
31
|
+
id: link.id,
|
|
32
|
+
itemUrl: link.itemUrl,
|
|
33
|
+
newTab: link.newTab,
|
|
34
|
+
});
|
|
35
|
+
link.preventDefault();
|
|
36
|
+
return undefined;
|
|
37
|
+
}, [onAppLinkClick]);
|
|
38
|
+
const handleEvent = useCallback((event) => {
|
|
39
|
+
switch (event.name) {
|
|
40
|
+
case "opened":
|
|
41
|
+
onEvent?.({ name: "chat.opened", payload: event.payload });
|
|
42
|
+
break;
|
|
43
|
+
case "closed":
|
|
44
|
+
onEvent?.({ name: "chat.closed", payload: event.payload });
|
|
45
|
+
break;
|
|
46
|
+
case "reset":
|
|
47
|
+
onEvent?.({ name: "chat.reset", payload: event.payload });
|
|
48
|
+
break;
|
|
49
|
+
case "feedback":
|
|
50
|
+
onEvent?.({ name: "chat.feedback", payload: event.payload });
|
|
51
|
+
break;
|
|
52
|
+
case "user-message":
|
|
53
|
+
onEvent?.({ name: "chat.user-message", payload: event.payload });
|
|
54
|
+
break;
|
|
55
|
+
case "assistant-message":
|
|
56
|
+
onEvent?.({ name: "chat.assistant-message", payload: event.payload });
|
|
57
|
+
break;
|
|
58
|
+
case "save-visualization-success":
|
|
59
|
+
addSuccess(defineMessage({ id: "messages.genAi.visualisation.saved.success" }));
|
|
60
|
+
break;
|
|
61
|
+
case "save-visualization-error":
|
|
62
|
+
addError(defineMessage({ id: "messages.genAi.visualisation.saved.error" }), {
|
|
63
|
+
showMore: intl.formatMessage({ id: "messages.showMore" }),
|
|
64
|
+
showLess: intl.formatMessage({ id: "messages.showLess" }),
|
|
65
|
+
errorDetail: intl.formatMessage({ id: "messages.genAi.visualisation.saved.error.detail" }, {
|
|
66
|
+
errorType: event.payload.errorType,
|
|
67
|
+
errorMessage: event.payload.errorMessage,
|
|
68
|
+
}),
|
|
69
|
+
duration: 0,
|
|
70
|
+
});
|
|
71
|
+
break;
|
|
72
|
+
case "copy-to-clipboard":
|
|
73
|
+
addSuccess(defineMessage({ id: "messages.genAi.visualisation.link.copied" }));
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
break;
|
|
22
77
|
}
|
|
23
|
-
return itemUrl;
|
|
24
|
-
}, []);
|
|
25
|
-
const events = useMemo(() => {
|
|
26
|
-
return [
|
|
27
|
-
{
|
|
28
|
-
eval: isChatOpenedEvent,
|
|
29
|
-
handler: (data) => onEvent?.({ name: "chat.opened", payload: data }),
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
eval: isChatClosedEvent,
|
|
33
|
-
handler: (data) => onEvent?.({ name: "chat.closed", payload: data }),
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
eval: isChatResetEvent,
|
|
37
|
-
handler: (data) => onEvent?.({ name: "chat.reset", payload: data }),
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
eval: isChatFeedbackEvent,
|
|
41
|
-
handler: (data) => onEvent?.({ name: "chat.feedback", payload: data }),
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
eval: isChatUserMessageEvent,
|
|
45
|
-
handler: (data) => onEvent?.({ name: "chat.user-message", payload: data }),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
eval: isChatAssistantMessageEvent,
|
|
49
|
-
handler: (data) => onEvent?.({ name: "chat.assistant-message", payload: data }),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
eval: isChatSaveVisualizationSuccessEvent,
|
|
53
|
-
handler: () => {
|
|
54
|
-
addSuccess(defineMessage({ id: "messages.genAi.visualisation.saved.success" }));
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
eval: isChatSaveVisualizationErrorEvent,
|
|
59
|
-
handler: ({ errorType, errorMessage }) => {
|
|
60
|
-
addError(defineMessage({ id: "messages.genAi.visualisation.saved.error" }), {
|
|
61
|
-
showMore: intl.formatMessage({ id: "messages.showMore" }),
|
|
62
|
-
showLess: intl.formatMessage({ id: "messages.showLess" }),
|
|
63
|
-
errorDetail: intl.formatMessage({ id: "messages.genAi.visualisation.saved.error.detail" }, {
|
|
64
|
-
errorType,
|
|
65
|
-
errorMessage,
|
|
66
|
-
}),
|
|
67
|
-
duration: 0,
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
eval: isChatCopyToClipboardEvent,
|
|
73
|
-
handler: () => {
|
|
74
|
-
addSuccess(defineMessage({ id: "messages.genAi.visualisation.link.copied" }));
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
];
|
|
78
78
|
}, [addError, addSuccess, intl, onEvent]);
|
|
79
|
-
|
|
80
|
-
const types = ["dashboard", "visualization"];
|
|
81
|
-
if (canManageProject) {
|
|
82
|
-
types.push("metric");
|
|
83
|
-
}
|
|
84
|
-
return types;
|
|
85
|
-
}, [canManageProject]);
|
|
86
|
-
const [chatDispatcher, setDispatcher] = useState(null);
|
|
87
|
-
const onDispatcher = useCallback((dispatcher) => {
|
|
88
|
-
setDispatcher(() => dispatcher);
|
|
89
|
-
}, []);
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
if (!chatDispatcher || !askedQuestion || !settings) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
chatDispatcher(clearThreadAction());
|
|
95
|
-
// Always set (and thereby clear when undefined) so a follow-up ask without context does not
|
|
96
|
-
// inherit the previous ask's user context (LX-2544).
|
|
97
|
-
chatDispatcher(setUserContextAction({ userContext }));
|
|
98
|
-
if (settings.enableAiAgenticConversations) {
|
|
99
|
-
chatDispatcher(newMessageAction(makeUserItem({ type: "text", text: askedQuestion })));
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
chatDispatcher(newMessageAction(makeUserMessage([makeTextContents(askedQuestion, [])])));
|
|
103
|
-
}
|
|
104
|
-
// `askSeq` is included so a repeated identical question (same `askedQuestion`) still re-seeds.
|
|
105
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
106
|
-
}, [chatDispatcher, askedQuestion, askSeq, userContext, settings]);
|
|
107
|
-
return (_jsx(GenAIChatDialog, { isOpen: open, locale: intl.locale, canManage: canManageProject, canAnalyze: canAnalyzeProject, canFullControl: canFullControl, objectTypes: objectTypes, includeTags: includeTags, excludeTags: excludeTags, onLinkClick: onLinkClick, onClose: onClose, onOpen: onOpen, workspace: workspaceId, backend: backend, settings: settings, eventHandlers: events, onDispatcher: onDispatcher, returnFocusTo: HEADER_CHAT_BUTTON_ID }));
|
|
79
|
+
return (_jsx(GenAIChatDialogConnected, { backend: backend, workspace: workspaceId, locale: intl.locale, isOpen: open, onOpen: onOpen, onClose: onClose, askedQuestion: askedQuestion, askSeq: askSeq, userContext: userContext, includeTags: includeTags, excludeTags: excludeTags, canManage: canManageProject, canAnalyze: canAnalyzeProject, canFullControl: canFullControl, settings: settings, dialogPosition: dialogPosition, allowNativeLinks: embedded ? false : undefined, className: embedded ? EMBEDDED_CHAT_CLASS : undefined, onLinkClick: embedded ? onLinkClick : undefined, onEvent: handleEvent, returnFocusTo: embedded ? EMBEDDED_AI_TRIGGER_ID : HEADER_CHAT_BUTTON_ID }));
|
|
108
80
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type IGenAIUserContext, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
import "@gooddata/sdk-ui-gen-ai/styles/css/main.css";
|
|
4
|
+
/**
|
|
5
|
+
* AI-assistant open/close/toggle request. `seq` changes on every request so an identical repeat
|
|
6
|
+
* (e.g. "Summarize" clicked again, or the header button toggled) still re-triggers the effect.
|
|
7
|
+
*/
|
|
8
|
+
export interface IHostChatVisibility {
|
|
9
|
+
kind: "open" | "close" | "toggle";
|
|
10
|
+
question?: string;
|
|
11
|
+
userContext?: IGenAIUserContext;
|
|
12
|
+
seq: number;
|
|
13
|
+
}
|
|
14
|
+
/** A link clicked inside the chat, delegated to the active application for in-app handling. */
|
|
15
|
+
export interface IHostChatLink {
|
|
16
|
+
type?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
itemUrl?: string;
|
|
19
|
+
newTab?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* AI-assistant presentation/context reported by the active pluggable application: its object-search
|
|
23
|
+
* tag scope plus, when embedded, how the host chat should be presented.
|
|
24
|
+
*/
|
|
25
|
+
export interface IHostChatContext {
|
|
26
|
+
includeTags?: string[];
|
|
27
|
+
excludeTags?: string[];
|
|
28
|
+
dialogPosition?: "left" | "right";
|
|
29
|
+
embedded?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface IHostChatProps {
|
|
32
|
+
ctx: IPlatformContext;
|
|
33
|
+
resolvedApplications: PluggableApplicationRegistryItem[];
|
|
34
|
+
pathname: string;
|
|
35
|
+
/** Id of the active pluggable application; used to reset tag scope on app switch. */
|
|
36
|
+
activeAppId?: string;
|
|
37
|
+
/** Latest open/close/toggle request (from app events, the header button, or header search). */
|
|
38
|
+
visibility?: IHostChatVisibility | null;
|
|
39
|
+
/** Latest tag scope / presentation reported by the active pluggable application. */
|
|
40
|
+
context?: IHostChatContext | null;
|
|
41
|
+
/** Reports the chat open-state so the runtime can forward it to the active app. */
|
|
42
|
+
onOpenChange?: (open: boolean) => void;
|
|
43
|
+
/** Reports the header chat-button state (visibility + open) so the host UI can render it. */
|
|
44
|
+
onChatStateChange?: (state: {
|
|
45
|
+
showChatItem: boolean;
|
|
46
|
+
isOpen: boolean;
|
|
47
|
+
}) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Delegates a chat link click to the active application (e.g. an embedded dashboard opening a
|
|
50
|
+
* visualization as an in-place overlay). Returns true if the app handled it.
|
|
51
|
+
*/
|
|
52
|
+
onAppLinkClick?: (link: IHostChatLink) => boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Owns and renders the host's single GenAI chat, decoupled from the host UI module.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* Rendered by {@link HostUiContainer} in its own React tree (with the providers the chat needs), so
|
|
59
|
+
* the chat survives a custom remote UI module and stays mounted when the chrome is hidden
|
|
60
|
+
* (embedded/export). The header chat button and app-side controls drive it through the `visibility`
|
|
61
|
+
* and `context` props; it reports its open-state and button state back via callbacks.
|
|
62
|
+
*/
|
|
63
|
+
export declare function HostChat({ ctx, resolvedApplications, pathname, activeAppId, visibility, context, onOpenChange, onChatStateChange, onAppLinkClick }: IHostChatProps): import("react/jsx-runtime").JSX.Element;
|
|
64
|
+
//# sourceMappingURL=HostChat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostChat.d.ts","sourceRoot":"","sources":["../../src/ui/HostChat.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAUlF,OAAO,6CAA6C,CAAC;AAErD;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACf;AAED,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,UAAU,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACxC,oFAAoF;IACpF,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAClC,mFAAmF;IACnF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACrB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,WAAW,EACX,UAAiB,EACjB,OAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACjB,EAAE,cAAc,2CA6EhB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// (C) 2026 GoodData Corporation
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { BackendProvider, resolveLocale } from "@gooddata/sdk-ui";
|
|
5
|
+
import { ToastsCenterContextProvider } from "@gooddata/sdk-ui-kit";
|
|
6
|
+
import { getAppLifecycleCallbacks } from "../loader/pluggableApplicationsLoader.js";
|
|
7
|
+
import { getBackend } from "../platformContext/backend.js";
|
|
8
|
+
import { HostIntlProvider } from "./HostIntlProvider.js";
|
|
9
|
+
import { useHostChromeChat } from "./useHostChromeChat.js";
|
|
10
|
+
import { useHostChromeWorkspaceFeatures } from "./useHostChromeWorkspaceFeatures.js";
|
|
11
|
+
import "@gooddata/sdk-ui-gen-ai/styles/css/main.css";
|
|
12
|
+
/**
|
|
13
|
+
* Owns and renders the host's single GenAI chat, decoupled from the host UI module.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Rendered by {@link HostUiContainer} in its own React tree (with the providers the chat needs), so
|
|
17
|
+
* the chat survives a custom remote UI module and stays mounted when the chrome is hidden
|
|
18
|
+
* (embedded/export). The header chat button and app-side controls drive it through the `visibility`
|
|
19
|
+
* and `context` props; it reports its open-state and button state back via callbacks.
|
|
20
|
+
*/
|
|
21
|
+
export function HostChat({ ctx, resolvedApplications, pathname, activeAppId, visibility = null, context = null, onOpenChange, onChatStateChange, onAppLinkClick, }) {
|
|
22
|
+
const features = useHostChromeWorkspaceFeatures(resolvedApplications, ctx, pathname);
|
|
23
|
+
const shellTelemetry = useMemo(() => getAppLifecycleCallbacks()?.createTelemetryCallbacks?.("host-ui"), []);
|
|
24
|
+
const chat = useHostChromeChat({
|
|
25
|
+
features,
|
|
26
|
+
ctx,
|
|
27
|
+
telemetry: shellTelemetry,
|
|
28
|
+
dialogPosition: context?.dialogPosition,
|
|
29
|
+
embedded: context?.embedded,
|
|
30
|
+
onAppLinkClick,
|
|
31
|
+
});
|
|
32
|
+
const { askAiAssistant: chatAskAiAssistant, open: chatOpen, close: chatClose, toggle: chatToggle, setTags: chatSetTags, isOpen: chatIsOpen, showChatItem, } = chat;
|
|
33
|
+
// Report the chat open-state outward so the runtime can forward it to the active pluggable
|
|
34
|
+
// application, keeping app-side assistant controls (and their echoed results) aligned (LX-2544).
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
onOpenChange?.(chatIsOpen);
|
|
37
|
+
}, [chatIsOpen, onOpenChange]);
|
|
38
|
+
// Report the header chat-button state so the host UI module can render the button to match.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
onChatStateChange?.({ showChatItem, isOpen: chatIsOpen });
|
|
41
|
+
}, [showChatItem, chatIsOpen, onChatStateChange]);
|
|
42
|
+
// Effect order matters: the tag-scope effects are declared before the open/ask effect so that,
|
|
43
|
+
// when a tag-scope change and an open/ask request land in the same commit, the host chat is
|
|
44
|
+
// already scoped before the seeded question is sent.
|
|
45
|
+
// Clear any stale tag scope when the active application changes. The newly active app re-reports
|
|
46
|
+
// its own scope (or none) right after it mounts; without this reset, switching from a tag-scoped
|
|
47
|
+
// app to one that reports no scope (e.g. the metric editor) would leak the old scope.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
chatSetTags(undefined, undefined);
|
|
50
|
+
}, [activeAppId, chatSetTags]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
chatSetTags(context?.includeTags, context?.excludeTags);
|
|
53
|
+
}, [context, chatSetTags]);
|
|
54
|
+
const visibilitySeq = visibility?.seq;
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!visibility) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (visibility.kind === "close") {
|
|
60
|
+
chatClose();
|
|
61
|
+
}
|
|
62
|
+
else if (visibility.kind === "toggle") {
|
|
63
|
+
chatToggle();
|
|
64
|
+
}
|
|
65
|
+
else if (visibility.question) {
|
|
66
|
+
chatAskAiAssistant(visibility.question, visibility.userContext);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
chatOpen();
|
|
70
|
+
}
|
|
71
|
+
// `seq` changes on every request, so a repeated identical open/ask/toggle still re-runs this.
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [visibilitySeq, chatAskAiAssistant, chatOpen, chatClose, chatToggle]);
|
|
74
|
+
return (_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsx(BackendProvider, { backend: getBackend(), children: _jsx(ToastsCenterContextProvider, { children: chat.element }) }) }));
|
|
75
|
+
}
|
package/esm/ui/HostChrome.d.ts
CHANGED
|
@@ -6,23 +6,6 @@ import "@gooddata/sdk-ui-ext/styles/css/main.css";
|
|
|
6
6
|
import "@gooddata/sdk-ui-gen-ai/styles/css/main.css";
|
|
7
7
|
import "@gooddata/sdk-ui-semantic-search/styles/css/main.css";
|
|
8
8
|
import "@gooddata/sdk-ui-semantic-search/styles/css/internal.css";
|
|
9
|
-
/**
|
|
10
|
-
* AI-assistant open/close request pushed from the active pluggable application. `seq` changes on
|
|
11
|
-
* every request so an identical repeat (e.g. "Summarize" clicked again) still re-triggers the host.
|
|
12
|
-
*/
|
|
13
|
-
export interface IHostChromeAiVisibility {
|
|
14
|
-
kind: "open" | "close";
|
|
15
|
-
question?: string;
|
|
16
|
-
userContext?: IGenAIUserContext;
|
|
17
|
-
seq: number;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* AI-assistant object-search tag scope reported by the active pluggable application.
|
|
21
|
-
*/
|
|
22
|
-
export interface IHostChromeAiContext {
|
|
23
|
-
includeTags?: string[];
|
|
24
|
-
excludeTags?: string[];
|
|
25
|
-
}
|
|
26
9
|
export interface IHostChromeProps {
|
|
27
10
|
ctx: IPlatformContext;
|
|
28
11
|
resolvedApplications: PluggableApplicationRegistryItem[];
|
|
@@ -31,12 +14,17 @@ export interface IHostChromeProps {
|
|
|
31
14
|
onReplace: (url: string) => void;
|
|
32
15
|
headerOptions?: IAppHeaderOptions;
|
|
33
16
|
notification?: IHostUiNotification | null;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Whether the header chat button should be shown. Reflects the host-owned chat's availability
|
|
19
|
+
* (feature flag, permissions and the runtime LLM-availability probe), pushed in by the runtime.
|
|
20
|
+
*/
|
|
21
|
+
showChatItem?: boolean;
|
|
22
|
+
/** Whether the host-owned chat is currently open (for the header button's active state). */
|
|
23
|
+
chatIsOpen?: boolean;
|
|
24
|
+
/** Toggles the host-owned chat; wired to the header chat button. */
|
|
25
|
+
onChatToggle?: () => void;
|
|
26
|
+
/** Hands a question to the host-owned chat; wired to the header search "ask AI" action. */
|
|
27
|
+
onAskAiAssistant?: (question: string, userContext?: IGenAIUserContext) => void;
|
|
40
28
|
/**
|
|
41
29
|
* Page-title segment set by the active pluggable application via a document-title-changed
|
|
42
30
|
* event. When omitted, the active application's manifest title is used instead.
|
|
@@ -44,5 +32,5 @@ export interface IHostChromeProps {
|
|
|
44
32
|
appPageTitle?: string;
|
|
45
33
|
children?: ReactNode;
|
|
46
34
|
}
|
|
47
|
-
export declare function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification,
|
|
35
|
+
export declare function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification, showChatItem, onChatToggle, onAskAiAssistant, appPageTitle, children }: IHostChromeProps): import("react/jsx-runtime").JSX.Element;
|
|
48
36
|
//# sourceMappingURL=HostChrome.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAEH,KAAK,iBAAiB,EAEtB,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AA+BnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAYlE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC/E;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CA6NlB"}
|
package/esm/ui/HostChrome.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// (C) 2026 GoodData Corporation
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
4
|
import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
|
|
5
5
|
import { BackendProvider, resolveLocale } from "@gooddata/sdk-ui";
|
|
6
6
|
import { AppHeaderNotifications } from "@gooddata/sdk-ui-application-header";
|
|
@@ -15,7 +15,6 @@ import { getUserDisplayName, swapWorkspaceInPath } from "./chromeHelpers.js";
|
|
|
15
15
|
import { b, e } from "./hostChromeBem.js";
|
|
16
16
|
import { HostIntlProvider } from "./HostIntlProvider.js";
|
|
17
17
|
import { HostNotificationDispatcher } from "./HostNotificationDispatcher.js";
|
|
18
|
-
import { useHostChromeChat } from "./useHostChromeChat.js";
|
|
19
18
|
import { useHostChromePricing } from "./useHostChromePricing.js";
|
|
20
19
|
import { useHostChromeSearch } from "./useHostChromeSearch.js";
|
|
21
20
|
import { useHostChromeWorkspaceFeatures } from "./useHostChromeWorkspaceFeatures.js";
|
|
@@ -36,60 +35,23 @@ const LOGOUT_MENU_ITEM_KEY = "gs.header.logout";
|
|
|
36
35
|
// favicon is configured (see faviconUrl below).
|
|
37
36
|
const initialFaviconUrl = (typeof document !== "undefined" && document.querySelector("link[rel~='icon']")?.getAttribute("href")) ||
|
|
38
37
|
"/favicon.ico";
|
|
39
|
-
export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification = null,
|
|
38
|
+
export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification = null, showChatItem = false, onChatToggle, onAskAiAssistant, appPageTitle, children, }) {
|
|
40
39
|
const locale = resolveLocale(ctx.preferredLocale);
|
|
41
40
|
const userName = getUserDisplayName(ctx.user);
|
|
42
41
|
const features = useHostChromeWorkspaceFeatures(resolvedApplications, ctx, pathname);
|
|
43
42
|
const shellTelemetry = useMemo(() => getAppLifecycleCallbacks()?.createTelemetryCallbacks?.("host-ui"), []);
|
|
44
43
|
const pricing = useHostChromePricing(ctx, locale);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}, [chatIsOpen, onAiAssistantOpenChange]);
|
|
53
|
-
// The active pluggable application must not render its own chat dialog on hosted routes — the
|
|
54
|
-
// chrome owns the single chat instance there (LX-2544). It drives that instance through these
|
|
55
|
-
// signals instead: the object-search tag scope of its current view and open/close requests.
|
|
56
|
-
//
|
|
57
|
-
// Effect order matters: the tag-scope effects are declared before the open/ask effect so that,
|
|
58
|
-
// when a tag-scope change and an open/ask request land in the same commit, the host chat is
|
|
59
|
-
// already scoped before the seeded question is sent.
|
|
60
|
-
// Clear any stale tag scope when the active application changes. The newly active app re-reports
|
|
61
|
-
// its own scope (or none) right after it mounts; without this reset, switching from a
|
|
62
|
-
// tag-scoped app to one that reports no scope (e.g. the metric editor) would leak the old scope.
|
|
63
|
-
// Declared before the context effect so that, in the rare commit where both change, the fresh
|
|
64
|
-
// scope wins over the reset.
|
|
65
|
-
const activeAppId = getActiveInternalApplication(resolvedApplications, ctx, pathname)?.id;
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
chatSetTags(undefined, undefined);
|
|
68
|
-
}, [activeAppId, chatSetTags]);
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
chatSetTags(aiContext?.includeTags, aiContext?.excludeTags);
|
|
71
|
-
}, [aiContext, chatSetTags]);
|
|
72
|
-
const aiVisibilitySeq = aiVisibility?.seq;
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!aiVisibility) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (aiVisibility.kind === "close") {
|
|
78
|
-
chatClose();
|
|
79
|
-
}
|
|
80
|
-
else if (aiVisibility.question) {
|
|
81
|
-
chatAskAiAssistant(aiVisibility.question, aiVisibility.userContext);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
chatOpen();
|
|
85
|
-
}
|
|
86
|
-
// `seq` changes on every request, so a repeated identical open/ask still re-runs this.
|
|
87
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
-
}, [aiVisibilitySeq, chatAskAiAssistant, chatOpen, chatClose]);
|
|
44
|
+
// The host's single GenAI chat is owned by the runtime (HostChat), not the chrome — so it survives
|
|
45
|
+
// a custom UI module and stays mounted when the chrome is hidden (embedded/export). The chrome only
|
|
46
|
+
// renders the header chat button, which toggles that chat via `onChatToggle`, and hands the header
|
|
47
|
+
// search's questions to it via `onAskAiAssistant`.
|
|
48
|
+
const handleAskAiAssistant = useCallback((question, userContext) => {
|
|
49
|
+
onAskAiAssistant?.(question, userContext);
|
|
50
|
+
}, [onAskAiAssistant]);
|
|
89
51
|
const search = useHostChromeSearch({
|
|
90
52
|
features,
|
|
91
53
|
isTrial: pricing.isTrial,
|
|
92
|
-
onAskAiAssistant:
|
|
54
|
+
onAskAiAssistant: handleAskAiAssistant,
|
|
93
55
|
telemetry: shellTelemetry,
|
|
94
56
|
});
|
|
95
57
|
const { menuItemsGroups, messages: appMessages } = useMemo(() => buildAppMenu(resolvedApplications, ctx, pathname, ctx.preferredLocale), [resolvedApplications, ctx, pathname]);
|
|
@@ -182,9 +144,6 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
|
|
|
182
144
|
const hideChrome = ctx.embeddingMode === "iframe" || ctx.isExportMode === true;
|
|
183
145
|
return (_jsx(HostIntlProvider, { locale: locale, additionalMessages: appMessages, children: _jsx(BackendProvider, { backend: getBackend(), children: _jsx(ToastsCenterContextProvider, { children: _jsxs("div", { className: b(), children: [
|
|
184
146
|
_jsx(DocumentHeader, { pageTitle: documentPageTitle, brandTitle: documentBrandTitle, faviconUrl: faviconUrl, appleTouchIconUrl: ctx.whiteLabeling?.appleTouchIconUrl }), hideChrome ? null : (_jsx("div", { className: e("header"), onMouseOver: handleHeaderMouseOver, children: _jsx(AppHeader, { logoUrl: ctx.whiteLabeling?.logoUrl || defaultLogoUrl, logoHref: "/organization" // switch the host scope to organization, the first org app will be chosen
|
|
185
|
-
, logoTitle: logoTitle, headerColor: headerColor, headerTextColor: headerTextColor, activeColor: activeColor, userName: userName, organizationName: ctx.organization?.title, isAccessibilityCompliant: true, workspacePicker: workspacePicker, menuItemsGroups: menuItemsGroups, helpMenuItems: helpMenuItems, accountMenuItems: accountMenuItems, onMenuItemClick: handleMenuItemClick, showUpsellButton: pricing.isTrial, onUpsellButtonClick: pricing.onUpsellButtonClick, expiredDate: pricing.isTrial ? pricing.expiredDate : undefined, search: search.element, showChatItem:
|
|
186
|
-
? ({ isMobile, closeNotificationsOverlay }) => (_jsx(AppHeaderNotifications, { locale: locale, isMobile: isMobile, closeNotificationsOverlay: closeNotificationsOverlay, useAsOfDateParam: ctx.userSettings.enableExecutionTimestamp ?? false, enableExportToDocumentStorage: ctx.userSettings.enableExportToDocumentStorage ??
|
|
187
|
-
false }))
|
|
188
|
-
: undefined }) })), _jsx("main", { className: e("content"), children: children }), hideChrome ? null : chat.element, pricing.element, _jsx(HostNotificationDispatcher, { notification: notification })
|
|
147
|
+
, logoTitle: logoTitle, headerColor: headerColor, headerTextColor: headerTextColor, activeColor: activeColor, userName: userName, organizationName: ctx.organization?.title, isAccessibilityCompliant: true, workspacePicker: workspacePicker, menuItemsGroups: menuItemsGroups, helpMenuItems: helpMenuItems, accountMenuItems: accountMenuItems, onMenuItemClick: handleMenuItemClick, showUpsellButton: pricing.isTrial, onUpsellButtonClick: pricing.onUpsellButtonClick, expiredDate: pricing.isTrial ? pricing.expiredDate : undefined, search: search.element, showChatItem: showChatItem, onChatItemClick: onChatToggle, notificationsPanel: ({ isMobile, closeNotificationsOverlay }) => (_jsx(AppHeaderNotifications, { locale: locale, isMobile: isMobile, closeNotificationsOverlay: closeNotificationsOverlay, useAsOfDateParam: true, enableExportToDocumentStorage: ctx.userSettings.enableExportToDocumentStorage ?? false })) }) })), _jsx("main", { className: e("content"), children: children }), pricing.element, _jsx(HostNotificationDispatcher, { notification: notification })
|
|
189
148
|
] }) }) }) }));
|
|
190
149
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import { type IGenAIUserContext, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
|
|
2
3
|
import { type IAppHeaderOptions, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
3
4
|
import "./PluggableApplicationRenderer.scss";
|
|
4
5
|
export interface IPluggableApplicationRendererProps {
|
|
@@ -7,8 +8,29 @@ export interface IPluggableApplicationRendererProps {
|
|
|
7
8
|
pathname: string;
|
|
8
9
|
/** Host-owned AI assistant chat open-state, forwarded to the mounted app's handle. */
|
|
9
10
|
aiAssistantOpen?: boolean;
|
|
11
|
+
/** Open/ask the host-owned chat, requested by the active app via an open-assistant event. */
|
|
12
|
+
onOpenAiAssistant?: (question?: string, userContext?: IGenAIUserContext) => void;
|
|
13
|
+
/** Close the host-owned chat, requested by the active app. */
|
|
14
|
+
onCloseAiAssistant?: () => void;
|
|
15
|
+
/** Report the active app's AI-assistant tag scope and presentation to the host-owned chat. */
|
|
16
|
+
onAiAssistantContext?: (context: {
|
|
17
|
+
includeTags?: string[];
|
|
18
|
+
excludeTags?: string[];
|
|
19
|
+
dialogPosition?: "left" | "right";
|
|
20
|
+
embedded?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Ref the renderer populates with a handler that delegates a host-chat link click to the active
|
|
24
|
+
* app's mount handle (`onAiAssistantLinkClicked`), so an embedded app can handle it in-app.
|
|
25
|
+
*/
|
|
26
|
+
aiLinkClickHandlerRef?: RefObject<((link: {
|
|
27
|
+
type?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
itemUrl?: string;
|
|
30
|
+
newTab?: boolean;
|
|
31
|
+
}) => boolean) | undefined>;
|
|
10
32
|
onHeaderChange?: (appId: string, header: IAppHeaderOptions) => void;
|
|
11
33
|
onDocumentTitleChange?: (appId: string, pageTitle: string | undefined) => void;
|
|
12
34
|
}
|
|
13
|
-
export declare function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onHeaderChange, onDocumentTitleChange }: IPluggableApplicationRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export declare function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, onHeaderChange, onDocumentTitleChange }: IPluggableApplicationRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
14
36
|
//# sourceMappingURL=PluggableApplicationRenderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAqD,MAAM,OAAO,CAAC;AAI1F,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EASxB,MAAM,2CAA2C,CAAC;AAcnD,OAAO,qCAAqC,CAAC;AAkB7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACjF,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,8FAA8F;IAC9F,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;KACtB,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,CAC7B,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CACtG,CAAC;IACF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CA0PpC"}
|
|
@@ -6,7 +6,6 @@ import { isAiAssistantContextChangedEvent, isCloseAiAssistantRequestedEvent, isD
|
|
|
6
6
|
import { LoadingComponent, useAutoupdateRef } from "@gooddata/sdk-ui";
|
|
7
7
|
import { bemFactory } from "@gooddata/sdk-ui-kit";
|
|
8
8
|
import { now } from "../debug.js";
|
|
9
|
-
import { dispatchHostNotification } from "../lib/hostNotifications.js";
|
|
10
9
|
import { getSecuredRemoteAppValidUntil, validateAppSecurity, } from "../loader/appSecurityValidation.js";
|
|
11
10
|
import { getAppLifecycleCallbacks, loadPluggableApplication } from "../loader/pluggableApplicationsLoader.js";
|
|
12
11
|
import { getApplicationHref } from "../loader/routing.js";
|
|
@@ -21,12 +20,15 @@ const SECURITY_FAILURE_MESSAGES = {
|
|
|
21
20
|
"build-expired": defineMessage({ id: "gs.host.error.appBuildExpired" }),
|
|
22
21
|
"metadata-missing": defineMessage({ id: "gs.host.error.appSecurityMetadataMissing" }),
|
|
23
22
|
};
|
|
24
|
-
export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onHeaderChange, onDocumentTitleChange, }) {
|
|
23
|
+
export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, onHeaderChange, onDocumentTitleChange, }) {
|
|
25
24
|
const intl = useIntl();
|
|
26
25
|
const intlRef = useAutoupdateRef(intl);
|
|
27
26
|
const ctxRef = useAutoupdateRef(ctx);
|
|
28
27
|
const onHeaderChangeRef = useAutoupdateRef(onHeaderChange);
|
|
29
28
|
const onDocumentTitleChangeRef = useAutoupdateRef(onDocumentTitleChange);
|
|
29
|
+
const onOpenAiAssistantRef = useAutoupdateRef(onOpenAiAssistant);
|
|
30
|
+
const onCloseAiAssistantRef = useAutoupdateRef(onCloseAiAssistant);
|
|
31
|
+
const onAiAssistantContextRef = useAutoupdateRef(onAiAssistantContext);
|
|
30
32
|
const containerRef = useRef(null);
|
|
31
33
|
const mountHandleRef = useRef(undefined);
|
|
32
34
|
// The app/module pair currently mounted. Held so the context-change effect can
|
|
@@ -47,32 +49,35 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
// The active pluggable application owns no chat dialog on hosted routes; it requests
|
|
50
|
-
// the host's single assistant through these events (open/ask and
|
|
51
|
-
// which the host
|
|
52
|
+
// the host's single (host-owned) assistant through these events (open/ask, close and
|
|
53
|
+
// tag-scope changes), which the host runtime drives directly via these callbacks.
|
|
52
54
|
if (isOpenAiAssistantRequestedEvent(event)) {
|
|
53
|
-
|
|
54
|
-
type: "openAiAssistant",
|
|
55
|
-
question: event.payload.question,
|
|
56
|
-
userContext: event.payload.userContext,
|
|
57
|
-
});
|
|
55
|
+
onOpenAiAssistantRef.current?.(event.payload.question, event.payload.userContext);
|
|
58
56
|
return;
|
|
59
57
|
}
|
|
60
58
|
if (isCloseAiAssistantRequestedEvent(event)) {
|
|
61
|
-
|
|
59
|
+
onCloseAiAssistantRef.current?.();
|
|
62
60
|
return;
|
|
63
61
|
}
|
|
64
62
|
if (isAiAssistantContextChangedEvent(event)) {
|
|
65
|
-
|
|
66
|
-
type: "aiAssistantContext",
|
|
63
|
+
onAiAssistantContextRef.current?.({
|
|
67
64
|
includeTags: event.payload.includeTags,
|
|
68
65
|
excludeTags: event.payload.excludeTags,
|
|
66
|
+
dialogPosition: event.payload.dialogPosition,
|
|
67
|
+
embedded: event.payload.embedded,
|
|
69
68
|
});
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
71
|
if (isDocumentTitleChangedEvent(event)) {
|
|
73
72
|
onDocumentTitleChangeRef.current?.(app.id, event.payload.pageTitle);
|
|
74
73
|
}
|
|
75
|
-
}, [
|
|
74
|
+
}, [
|
|
75
|
+
app.id,
|
|
76
|
+
onDocumentTitleChangeRef,
|
|
77
|
+
onOpenAiAssistantRef,
|
|
78
|
+
onCloseAiAssistantRef,
|
|
79
|
+
onAiAssistantContextRef,
|
|
80
|
+
]);
|
|
76
81
|
useEffect(() => {
|
|
77
82
|
let cancelled = false;
|
|
78
83
|
const mountId = `${app.id}:${Date.now()}`;
|
|
@@ -181,6 +186,19 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
181
186
|
}
|
|
182
187
|
mountHandleRef.current?.setAiAssistantOpen?.(aiAssistantOpen);
|
|
183
188
|
}, [aiAssistantOpen, viewState.state]);
|
|
189
|
+
// Expose a host-chat link-click delegate that defers to the active app's mount handle, so an
|
|
190
|
+
// embedded app can handle clicks in-app (e.g. open a visualization overlay) rather than navigating.
|
|
191
|
+
// Read through the ref at call time so it always targets the currently mounted app.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const ref = aiLinkClickHandlerRef;
|
|
194
|
+
if (!ref) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
ref.current = (link) => mountHandleRef.current?.onAiAssistantLinkClicked?.(link) ?? false;
|
|
198
|
+
return () => {
|
|
199
|
+
ref.current = undefined;
|
|
200
|
+
};
|
|
201
|
+
}, [aiLinkClickHandlerRef]);
|
|
184
202
|
return (_jsxs("section", { className: b(), children: [viewState.state === "loading" ? (_jsx("div", { className: e("loading"), children: _jsx(LoadingComponent, { height: 40 }) })) : null, viewState.state === "error" ? (_jsxs("div", { className: e("error"), children: [
|
|
185
203
|
_jsx("h2", { children: _jsx(FormattedMessage, { id: "gs.host.error.applicationFailedToLoad" }) }), _jsx("p", { children: viewState.message })
|
|
186
204
|
] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) }), viewState.state === "ready" && viewState.validUntil !== undefined ? (_jsx("div", { className: e("demoBadge"), role: "status", children: _jsx(FormattedMessage, { id: "gs.host.demoApp.validUntil", values: {
|