jupyter-chat-components 0.1.2 → 0.2.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 CHANGED
@@ -2,7 +2,37 @@
2
2
 
3
3
  [![Github Actions Status](https://github.com/brichet/jupyter-chat-components/workflows/Build/badge.svg)](https://github.com/brichet/jupyter-chat-components/actions/workflows/build.yml)
4
4
 
5
- Components to displayed in jupyter chat
5
+ A library of React components designed for use in Jupyter chat interfaces, with a focus on AI-powered interactions. These components are intended to be integrated into JupyterLab extensions that provide chat functionality.
6
+
7
+ ## MIME renderer
8
+
9
+ Components are exposed through a custom MIME type: `application/vnd.jupyter.chat.components`.
10
+
11
+ This extension registers a MIME renderer factory with JupyterLab's render MIME registry. To display a component, produce output with the MIME type above, where:
12
+
13
+ - the **data** value is the component name (e.g. `"tool-call"`)
14
+ - the **metadata** contains the props to pass to the component
15
+
16
+ The MIME renderer looks up the component name in the factory's registry and renders the corresponding React component.
17
+
18
+ ## Component registry
19
+
20
+ The registry is available directly on the `IComponentsRendererFactory` token as the `registry` property. It maps component names to React components and exposes the following methods:
21
+
22
+ - `add(name, component)` — register a new React component under a unique name
23
+ - `get(name)` — retrieve a registered component by name
24
+ - `has(name)` — check whether a component is registered
25
+ - `getNames()` — list all registered component names
26
+
27
+ Other JupyterLab extensions can consume the `IComponentsRendererFactory` token and use `registry.add()` to register their own components, which will then be available for rendering via the MIME bundle.
28
+
29
+ ## Available components
30
+
31
+ ### `tool-call`
32
+
33
+ Renders an AI tool call, displaying the tool name, input arguments, and output in a structured and readable format. Useful for visualizing function calls made by AI assistants during a conversation.
34
+
35
+ More components are planned for future releases.
6
36
 
7
37
  ## Requirements
8
38
 
@@ -0,0 +1,3 @@
1
+ /**
2
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3
+ */
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
4
+ */
5
+ describe('jupyter-chat-components', () => {
6
+ it('should be tested', () => {
7
+ expect(1 + 1).toEqual(2);
8
+ });
9
+ });
@@ -0,0 +1,2 @@
1
+ export * from './inline-diff';
2
+ export * from './tool-call';
@@ -0,0 +1,2 @@
1
+ export * from './inline-diff';
2
+ export * from './tool-call';
@@ -0,0 +1,11 @@
1
+ import { TranslationBundle } from '@jupyterlab/translation';
2
+ import * as React from 'react';
3
+ import { IInlineDiffMetadata } from '../token';
4
+ export interface IInlineDiffProps extends IInlineDiffMetadata {
5
+ trans?: TranslationBundle;
6
+ }
7
+ export declare function getDiffFilename(path: string): string;
8
+ /**
9
+ * React component for rendering one or more inline file diffs.
10
+ */
11
+ export declare const InlineDiff: React.FC<IInlineDiffProps>;
@@ -0,0 +1,82 @@
1
+ import { PathExt } from '@jupyterlab/coreutils';
2
+ import { nullTranslator } from '@jupyterlab/translation';
3
+ import * as React from 'react';
4
+ import { structuredPatch } from 'diff';
5
+ /** Maximum number of rendered lines before truncation. */
6
+ const MAX_DIFF_LINES = 20;
7
+ export function getDiffFilename(path) {
8
+ return PathExt.basename(path);
9
+ }
10
+ function toLineInfo(type, text, key) {
11
+ switch (type) {
12
+ case 'added':
13
+ return {
14
+ cssClass: 'jp-mod-added',
15
+ prefix: '+',
16
+ text,
17
+ key
18
+ };
19
+ case 'removed':
20
+ return {
21
+ cssClass: 'jp-mod-removed',
22
+ prefix: '-',
23
+ text,
24
+ key
25
+ };
26
+ case 'context':
27
+ return {
28
+ cssClass: 'jp-mod-context',
29
+ prefix: ' ',
30
+ text,
31
+ key
32
+ };
33
+ }
34
+ }
35
+ function buildDiffLinesFromHunk(hunk, hunkIndex) {
36
+ return hunk.lines
37
+ .filter(line => !line.startsWith('\\'))
38
+ .map((line, lineIndex) => {
39
+ var _a;
40
+ const prefix = (_a = line[0]) !== null && _a !== void 0 ? _a : ' ';
41
+ const text = line.slice(1);
42
+ const key = `${hunkIndex}-${hunk.oldStart}-${hunk.newStart}-${lineIndex}`;
43
+ if (prefix === '+') {
44
+ return toLineInfo('added', text, key);
45
+ }
46
+ if (prefix === '-') {
47
+ return toLineInfo('removed', text, key);
48
+ }
49
+ return toLineInfo('context', text, key);
50
+ });
51
+ }
52
+ function buildDiffLines(diff) {
53
+ var _a;
54
+ const patch = structuredPatch(diff.path, diff.path, (_a = diff.oldText) !== null && _a !== void 0 ? _a : '', diff.newText, undefined, undefined, { context: Infinity });
55
+ return patch.hunks.reduce((lines, hunk, index) => {
56
+ lines.push(...buildDiffLinesFromHunk(hunk, index));
57
+ return lines;
58
+ }, []);
59
+ }
60
+ function DiffBlock({ diff, trans }) {
61
+ const filename = getDiffFilename(diff.path);
62
+ const [expanded, setExpanded] = React.useState(false);
63
+ const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
64
+ const canTruncate = allLines.length > MAX_DIFF_LINES;
65
+ const visibleLines = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
66
+ const hiddenCount = allLines.length - MAX_DIFF_LINES;
67
+ return (React.createElement("div", { className: "jp-ai-inline-diff-block" },
68
+ React.createElement("div", { className: "jp-ai-inline-diff-header", title: diff.path }, filename),
69
+ React.createElement("div", { className: "jp-ai-inline-diff-content" },
70
+ visibleLines.length ? (visibleLines.map(line => (React.createElement("div", { key: line.key, className: `jp-ai-inline-diff-line ${line.cssClass}` },
71
+ React.createElement("span", { className: "jp-ai-inline-diff-line-prefix" }, line.prefix),
72
+ React.createElement("span", { className: "jp-ai-inline-diff-line-text" }, line.text))))) : (React.createElement("div", { className: "jp-ai-inline-diff-empty" }, trans.__('No changes'))),
73
+ canTruncate && !expanded && (React.createElement("button", { className: "jp-ai-inline-diff-toggle", onClick: () => setExpanded(true), type: "button" }, trans.__('... %1 more lines', hiddenCount))),
74
+ canTruncate && expanded && (React.createElement("button", { className: "jp-ai-inline-diff-toggle", onClick: () => setExpanded(false), type: "button" }, trans.__('Show less'))))));
75
+ }
76
+ /**
77
+ * React component for rendering one or more inline file diffs.
78
+ */
79
+ export const InlineDiff = ({ diffs, trans }) => {
80
+ const transBundle = trans !== null && trans !== void 0 ? trans : nullTranslator.load('jupyterlab');
81
+ return (React.createElement("div", { className: "jp-ai-inline-diff-container" }, diffs.map((diff, index) => (React.createElement(DiffBlock, { key: `${diff.path}-${index}`, diff: diff, trans: transBundle })))));
82
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { IComponentProps, ToolCallApproval } from '../token';
3
+ /**
4
+ * Tool call status types.
5
+ */
6
+ export type ToolCallStatus = 'pending' | 'awaiting_approval' | 'approved' | 'rejected' | 'completed' | 'error';
7
+ /**
8
+ * Options for building tool call HTML.
9
+ */
10
+ export interface IToolCallMetadata {
11
+ toolName: string;
12
+ input: string;
13
+ status: ToolCallStatus;
14
+ summary?: string;
15
+ output?: string;
16
+ targetId?: string;
17
+ approvalId?: string;
18
+ }
19
+ /**
20
+ * Options for building tool call HTML.
21
+ */
22
+ export interface IToolCallProps extends IComponentProps, IToolCallMetadata {
23
+ toolCallApproval?: ToolCallApproval;
24
+ }
25
+ export declare function escapeHtml(value: string): string;
26
+ /**
27
+ * React functional component for displaying a tool call.
28
+ *
29
+ * Renders a collapsible details element showing tool execution information
30
+ * including input, output, and approval buttons if needed.
31
+ */
32
+ export declare const ToolCall: React.FC<IToolCallProps>;
@@ -0,0 +1,96 @@
1
+ import * as React from 'react';
2
+ const STATUS_CONFIG = {
3
+ pending: {
4
+ cssClass: 'jp-ai-tool-pending',
5
+ statusClass: 'jp-ai-tool-status-pending'
6
+ },
7
+ awaiting_approval: {
8
+ cssClass: 'jp-ai-tool-pending',
9
+ statusClass: 'jp-ai-tool-status-approval',
10
+ open: true
11
+ },
12
+ approved: {
13
+ cssClass: 'jp-ai-tool-pending',
14
+ statusClass: 'jp-ai-tool-status-completed'
15
+ },
16
+ rejected: {
17
+ cssClass: 'jp-ai-tool-error',
18
+ statusClass: 'jp-ai-tool-status-error'
19
+ },
20
+ completed: {
21
+ cssClass: 'jp-ai-tool-completed',
22
+ statusClass: 'jp-ai-tool-status-completed'
23
+ },
24
+ error: {
25
+ cssClass: 'jp-ai-tool-error',
26
+ statusClass: 'jp-ai-tool-status-error'
27
+ }
28
+ };
29
+ export function escapeHtml(value) {
30
+ // Prefer the same native escaping approach used in JupyterLab itself
31
+ // (e.g. `@jupyterlab/completer`).
32
+ if (typeof document !== 'undefined') {
33
+ const node = document.createElement('span');
34
+ node.textContent = value;
35
+ return node.innerHTML;
36
+ }
37
+ // Fallback
38
+ return value
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/</g, '&lt;')
41
+ .replace(/>/g, '&gt;')
42
+ .replace(/"/g, '&quot;')
43
+ .replace(/'/g, '&#39;');
44
+ }
45
+ /**
46
+ * Returns the translated status text for a given tool status.
47
+ */
48
+ const getStatusText = (status, trans) => {
49
+ switch (status) {
50
+ case 'pending':
51
+ return trans.__('Running...');
52
+ case 'awaiting_approval':
53
+ return trans.__('Awaiting Approval');
54
+ case 'approved':
55
+ return trans.__('Approved - Executing...');
56
+ case 'rejected':
57
+ return trans.__('Rejected');
58
+ case 'completed':
59
+ return trans.__('Completed');
60
+ case 'error':
61
+ return trans.__('Error');
62
+ }
63
+ };
64
+ /**
65
+ * React functional component for displaying a tool call.
66
+ *
67
+ * Renders a collapsible details element showing tool execution information
68
+ * including input, output, and approval buttons if needed.
69
+ */
70
+ export const ToolCall = ({ toolName, input, status, summary, output, targetId, approvalId, trans, toolCallApproval }) => {
71
+ const config = STATUS_CONFIG[status];
72
+ const statusText = getStatusText(status, trans);
73
+ const resultLabel = status === 'error' ? trans.__('Error') : trans.__('Result');
74
+ if (status === 'awaiting_approval' && !toolCallApproval) {
75
+ console.error('The tool call has no approval function, approval, it will not work as expected');
76
+ }
77
+ return (React.createElement("details", { className: `jp-ai-tool-call ${config.cssClass}`, open: config.open },
78
+ React.createElement("summary", { className: "jp-ai-tool-header" },
79
+ React.createElement("div", { className: "jp-ai-tool-icon" }, "\u26A1"),
80
+ React.createElement("div", { className: "jp-ai-tool-title" },
81
+ toolName,
82
+ summary && React.createElement("span", { className: "jp-ai-tool-summary" }, summary)),
83
+ React.createElement("div", { className: `jp-ai-tool-status ${config.statusClass}` }, statusText)),
84
+ React.createElement("div", { className: "jp-ai-tool-body" },
85
+ React.createElement("div", { className: "jp-ai-tool-section" },
86
+ React.createElement("div", { className: "jp-ai-tool-label" }, trans.__('Input')),
87
+ React.createElement("pre", { className: "jp-ai-tool-code" },
88
+ React.createElement("code", null, input))),
89
+ status === 'awaiting_approval' && approvalId && targetId && (React.createElement("div", { className: `jp-ai-tool-approval-buttons jp-ai-approval-id--${approvalId}` },
90
+ React.createElement("button", { className: "jp-ai-approval-btn jp-ai-approval-approve", onClick: () => toolCallApproval === null || toolCallApproval === void 0 ? void 0 : toolCallApproval(targetId, approvalId, true) }, trans.__('Approve')),
91
+ React.createElement("button", { className: "jp-ai-approval-btn jp-ai-approval-reject", onClick: () => toolCallApproval === null || toolCallApproval === void 0 ? void 0 : toolCallApproval(targetId, approvalId, false) }, trans.__('Reject')))),
92
+ output !== undefined && (React.createElement("div", { className: "jp-ai-tool-section" },
93
+ React.createElement("div", { className: "jp-ai-tool-label" }, resultLabel),
94
+ React.createElement("pre", { className: "jp-ai-tool-code" },
95
+ React.createElement("code", null, output)))))));
96
+ };
@@ -0,0 +1,52 @@
1
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import * as React from 'react';
4
+ import { ComponentRegistry } from './registry';
5
+ import { IComponentRegistry, IComponentsRendererFactory, ToolCallApproval } from './token';
6
+ type ReactRenderElement = Array<React.ReactElement<any>> | React.ReactElement<any>;
7
+ /**
8
+ * The options for the chat components renderer.
9
+ */
10
+ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
11
+ /**
12
+ * The callback to approve or reject a tool.
13
+ */
14
+ toolCallApproval?: ToolCallApproval;
15
+ /**
16
+ * The component registry.
17
+ */
18
+ registry: IComponentRegistry;
19
+ }
20
+ /**
21
+ * A widget for rendering components from mime bundle.
22
+ */
23
+ export declare class ComponentsRenderer extends ReactWidget implements IRenderMime.IRenderer {
24
+ /**
25
+ * Construct a new output widget.
26
+ */
27
+ constructor(options: IComponentsRendererOptions);
28
+ /**
29
+ * Render into this widget's node.
30
+ */
31
+ renderModel(model: IRenderMime.IMimeModel): Promise<void>;
32
+ protected render(): ReactRenderElement | null;
33
+ private _trans;
34
+ private _mimeType;
35
+ private _toolCallApproval?;
36
+ private _registry;
37
+ private _data;
38
+ private _metadata;
39
+ }
40
+ /**
41
+ * A mime renderer factory for chat components.
42
+ */
43
+ export declare class RendererFactory implements IComponentsRendererFactory {
44
+ readonly safe = true;
45
+ readonly mimeTypes: string[];
46
+ readonly defaultRank = 100;
47
+ readonly registry: ComponentRegistry;
48
+ toolCallApproval: ToolCallApproval;
49
+ constructor();
50
+ createRenderer: (options: IRenderMime.IRendererOptions) => ComponentsRenderer;
51
+ }
52
+ export {};
package/lib/factory.js ADDED
@@ -0,0 +1,79 @@
1
+ import { nullTranslator } from '@jupyterlab/translation';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import * as React from 'react';
4
+ import { InlineDiff, ToolCall } from './components';
5
+ import { ComponentRegistry } from './registry';
6
+ /**
7
+ * The default mime type for the extension.
8
+ */
9
+ const MIME_TYPE = 'application/vnd.jupyter.chat.components';
10
+ /**
11
+ * The class name added to the extension.
12
+ */
13
+ const CLASS_NAME = 'jp-RenderedChatComponents';
14
+ /**
15
+ * A widget for rendering components from mime bundle.
16
+ */
17
+ export class ComponentsRenderer extends ReactWidget {
18
+ /**
19
+ * Construct a new output widget.
20
+ */
21
+ constructor(options) {
22
+ var _a;
23
+ super();
24
+ this._data = null;
25
+ this._metadata = null;
26
+ this._trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load('jupyterlab');
27
+ this._mimeType = options.mimeType;
28
+ this._toolCallApproval = options.toolCallApproval;
29
+ this._registry = options.registry;
30
+ this.addClass(CLASS_NAME);
31
+ }
32
+ /**
33
+ * Render into this widget's node.
34
+ */
35
+ async renderModel(model) {
36
+ var _a;
37
+ this._data = model.data[this._mimeType];
38
+ const metadata = model.metadata;
39
+ this._metadata = (_a = metadata[this._mimeType]) !== null && _a !== void 0 ? _a : {
40
+ ...metadata
41
+ };
42
+ return this.update();
43
+ }
44
+ render() {
45
+ if (!this._data) {
46
+ return null;
47
+ }
48
+ const Component = this._registry.get(this._data);
49
+ if (!Component) {
50
+ return null;
51
+ }
52
+ const componentsProps = { ...this._metadata };
53
+ if (this._data === 'tool-call') {
54
+ componentsProps.toolCallApproval = this._toolCallApproval;
55
+ }
56
+ return React.createElement(Component, { ...componentsProps, trans: this._trans });
57
+ }
58
+ }
59
+ /**
60
+ * A mime renderer factory for chat components.
61
+ */
62
+ export class RendererFactory {
63
+ constructor() {
64
+ this.safe = true;
65
+ this.mimeTypes = [MIME_TYPE];
66
+ this.defaultRank = 100;
67
+ this.toolCallApproval = null;
68
+ this.createRenderer = (options) => {
69
+ return new ComponentsRenderer({
70
+ ...options,
71
+ toolCallApproval: this.toolCallApproval,
72
+ registry: this.registry
73
+ });
74
+ };
75
+ this.registry = new ComponentRegistry();
76
+ this.registry.add('tool-call', ToolCall);
77
+ this.registry.add('inline-diff', InlineDiff);
78
+ }
79
+ }
package/lib/index.d.ts CHANGED
@@ -1,32 +1,11 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
- import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
3
- import { Widget } from '@lumino/widgets';
4
- import { IComponentsRendererFactory, ToolCallApproval } from './token';
2
+ import { IComponentsRendererFactory } from './token';
5
3
  /**
6
- * The options for the chat components renderer.
4
+ * The plugin providing the chat component renderer.
7
5
  */
8
- interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
9
- /**
10
- * The callback to approve or reject a tool.
11
- */
12
- toolCallApproval: ToolCallApproval;
13
- }
14
- /**
15
- * A widget for rendering .
16
- */
17
- export declare class ComponentsRenderer extends Widget implements IRenderMime.IRenderer {
18
- /**
19
- * Construct a new output widget.
20
- */
21
- constructor(options: IComponentsRendererOptions);
22
- /**
23
- * Render into this widget's node.
24
- */
25
- renderModel(model: IRenderMime.IMimeModel): Promise<void>;
26
- private _trans;
27
- private _mimeType;
28
- private _toolCallApproval;
29
- }
30
- declare const plugin: JupyterFrontEndPlugin<IComponentsRendererFactory>;
6
+ declare const factory: JupyterFrontEndPlugin<IComponentsRendererFactory>;
31
7
  export * from './token';
32
- export default plugin;
8
+ export * from './factory';
9
+ export * from './registry';
10
+ export * from './components';
11
+ export default factory;
package/lib/index.js CHANGED
@@ -1,67 +1,11 @@
1
1
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
- import { nullTranslator } from '@jupyterlab/translation';
3
- import { Widget } from '@lumino/widgets';
2
+ import { RendererFactory } from './factory';
4
3
  import { IComponentsRendererFactory } from './token';
5
- import { buildToolCallHtml } from './tool-call';
6
4
  /**
7
- * The default mime type for the extension.
5
+ * The plugin providing the chat component renderer.
8
6
  */
9
- const MIME_TYPE = 'application/vnd.jupyter.chat.components';
10
- /**
11
- * The class name added to the extension.
12
- */
13
- const CLASS_NAME = 'jp-RenderedChatComponents';
14
- /**
15
- * A widget for rendering .
16
- */
17
- export class ComponentsRenderer extends Widget {
18
- /**
19
- * Construct a new output widget.
20
- */
21
- constructor(options) {
22
- var _a;
23
- super();
24
- this._trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load('jupyterlab');
25
- this._mimeType = options.mimeType;
26
- this._toolCallApproval = options.toolCallApproval;
27
- this.addClass(CLASS_NAME);
28
- }
29
- /**
30
- * Render into this widget's node.
31
- */
32
- renderModel(model) {
33
- const data = model.data[this._mimeType];
34
- const metadata = { ...model.metadata };
35
- if (data === 'tool-call') {
36
- const toolCallOptions = {
37
- ...metadata,
38
- trans: this._trans,
39
- toolCallApproval: this._toolCallApproval
40
- };
41
- this.node.appendChild(buildToolCallHtml(toolCallOptions));
42
- }
43
- return Promise.resolve();
44
- }
45
- }
46
- /**
47
- * A mime renderer factory for chat components.
48
- */
49
- class RendererFactory {
50
- constructor() {
51
- this.safe = true;
52
- this.mimeTypes = [MIME_TYPE];
53
- this.defaultRank = 100;
54
- this.toolCallApproval = null;
55
- this.createRenderer = (options) => {
56
- return new ComponentsRenderer({
57
- ...options,
58
- toolCallApproval: this.toolCallApproval
59
- });
60
- };
61
- }
62
- }
63
- const plugin = {
64
- id: 'jupyter-chat-components:plugin',
7
+ const factory = {
8
+ id: 'jupyter-chat-components:factory',
65
9
  description: 'Adds MIME type renderer for chat components',
66
10
  autoStart: true,
67
11
  provides: IComponentsRendererFactory,
@@ -73,4 +17,7 @@ const plugin = {
73
17
  }
74
18
  };
75
19
  export * from './token';
76
- export default plugin;
20
+ export * from './factory';
21
+ export * from './registry';
22
+ export * from './components';
23
+ export default factory;
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { IComponentRegistry } from './token';
3
+ /**
4
+ * A registry for React components.
5
+ */
6
+ export declare class ComponentRegistry implements IComponentRegistry {
7
+ /**
8
+ * Register a React component.
9
+ *
10
+ * @param name - The unique name/identifier for the component
11
+ * @param component - The React component
12
+ */
13
+ add(name: string, component: React.ComponentType<any>): void;
14
+ /**
15
+ * Get a registered component by name.
16
+ *
17
+ * @param name - The name of the component
18
+ * @returns the React component, or undefined if not found
19
+ */
20
+ get(name: string): React.ComponentType<any> | undefined;
21
+ /**
22
+ * Check if a component is registered.
23
+ *
24
+ * @param name - The name of the component
25
+ * @returns whether the component is registered
26
+ */
27
+ has(name: string): boolean;
28
+ /**
29
+ * Get all registered component names.
30
+ *
31
+ * @returns the component names
32
+ */
33
+ getNames(): string[];
34
+ private _components;
35
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A registry for React components.
3
+ */
4
+ export class ComponentRegistry {
5
+ constructor() {
6
+ this._components = new Map();
7
+ }
8
+ /**
9
+ * Register a React component.
10
+ *
11
+ * @param name - The unique name/identifier for the component
12
+ * @param component - The React component
13
+ */
14
+ add(name, component) {
15
+ if (this._components.has(name)) {
16
+ console.warn(`Component '${name}' is already registered and will be overwritten.`);
17
+ }
18
+ this._components.set(name, component);
19
+ }
20
+ /**
21
+ * Get a registered component by name.
22
+ *
23
+ * @param name - The name of the component
24
+ * @returns the React component, or undefined if not found
25
+ */
26
+ get(name) {
27
+ return this._components.get(name);
28
+ }
29
+ /**
30
+ * Check if a component is registered.
31
+ *
32
+ * @param name - The name of the component
33
+ * @returns whether the component is registered
34
+ */
35
+ has(name) {
36
+ return this._components.has(name);
37
+ }
38
+ /**
39
+ * Get all registered component names.
40
+ *
41
+ * @returns the component names
42
+ */
43
+ getNames() {
44
+ return Array.from(this._components.keys());
45
+ }
46
+ }