@jupyter/chat 0.1.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/lib/__tests__/model.spec.d.ts +1 -0
- package/lib/__tests__/model.spec.js +72 -0
- package/lib/__tests__/widgets.spec.d.ts +1 -0
- package/lib/__tests__/widgets.spec.js +33 -0
- package/lib/components/chat-input.d.ts +33 -0
- package/lib/components/chat-input.js +60 -0
- package/lib/components/chat-messages.d.ts +32 -0
- package/lib/components/chat-messages.js +162 -0
- package/lib/components/chat.d.ts +43 -0
- package/lib/components/chat.js +100 -0
- package/lib/components/copy-button.d.ts +6 -0
- package/lib/components/copy-button.js +35 -0
- package/lib/components/jl-theme-provider.d.ts +6 -0
- package/lib/components/jl-theme-provider.js +19 -0
- package/lib/components/mui-extras/stacking-alert.d.ts +28 -0
- package/lib/components/mui-extras/stacking-alert.js +56 -0
- package/lib/components/rendermime-markdown.d.ts +12 -0
- package/lib/components/rendermime-markdown.js +54 -0
- package/lib/components/scroll-container.d.ts +23 -0
- package/lib/components/scroll-container.js +51 -0
- package/lib/components/toolbar.d.ts +11 -0
- package/lib/components/toolbar.js +30 -0
- package/lib/icons.d.ts +2 -0
- package/lib/icons.js +11 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +10 -0
- package/lib/model.d.ts +177 -0
- package/lib/model.js +128 -0
- package/lib/theme-provider.d.ts +3 -0
- package/lib/theme-provider.js +133 -0
- package/lib/types.d.ts +49 -0
- package/lib/types.js +5 -0
- package/lib/widgets/chat-error.d.ts +2 -0
- package/lib/widgets/chat-error.js +26 -0
- package/lib/widgets/chat-sidebar.d.ts +4 -0
- package/lib/widgets/chat-sidebar.js +15 -0
- package/lib/widgets/chat-widget.d.ts +19 -0
- package/lib/widgets/chat-widget.js +28 -0
- package/package.json +209 -0
- package/src/__tests__/model.spec.ts +84 -0
- package/src/__tests__/widgets.spec.ts +43 -0
- package/src/components/chat-input.tsx +143 -0
- package/src/components/chat-messages.tsx +283 -0
- package/src/components/chat.tsx +179 -0
- package/src/components/copy-button.tsx +55 -0
- package/src/components/jl-theme-provider.tsx +28 -0
- package/src/components/mui-extras/stacking-alert.tsx +105 -0
- package/src/components/rendermime-markdown.tsx +88 -0
- package/src/components/scroll-container.tsx +74 -0
- package/src/components/toolbar.tsx +50 -0
- package/src/icons.ts +15 -0
- package/src/index.ts +11 -0
- package/src/model.ts +272 -0
- package/src/theme-provider.ts +137 -0
- package/src/types/mui.d.ts +18 -0
- package/src/types/svg.d.ts +17 -0
- package/src/types.ts +58 -0
- package/src/widgets/chat-error.tsx +43 -0
- package/src/widgets/chat-sidebar.tsx +30 -0
- package/src/widgets/chat-widget.tsx +51 -0
- package/style/base.css +13 -0
- package/style/chat-settings.css +10 -0
- package/style/chat.css +53 -0
- package/style/icons/chat.svg +6 -0
- package/style/index.css +6 -0
- package/style/index.js +6 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
7
|
+
import { Alert, AlertColor, Collapse } from '@mui/material';
|
|
8
|
+
|
|
9
|
+
export type StackingAlert = {
|
|
10
|
+
/**
|
|
11
|
+
* A function that triggers an alert. Successive alerts are indicated in the
|
|
12
|
+
* JSX element.
|
|
13
|
+
* @param alertType Type of alert.
|
|
14
|
+
* @param msg Message contained within the alert.
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
show: (alertType: AlertColor, msg: string | Error) => void;
|
|
18
|
+
/**
|
|
19
|
+
* The Alert JSX element that should be rendered by the consumer.
|
|
20
|
+
* This will be `null` if no alerts were triggered.
|
|
21
|
+
*/
|
|
22
|
+
jsx: JSX.Element | null;
|
|
23
|
+
/**
|
|
24
|
+
* An async function that closes the alert, and returns a Promise that
|
|
25
|
+
* resolves when the onClose animation is completed.
|
|
26
|
+
*/
|
|
27
|
+
clear: () => void | Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook that returns a function to trigger an alert, and a corresponding alert
|
|
32
|
+
* JSX element for the consumer to render. The number of successive identical
|
|
33
|
+
* alerts `X` is indicated in the element via the suffix "(X)".
|
|
34
|
+
*/
|
|
35
|
+
export function useStackingAlert(): StackingAlert {
|
|
36
|
+
const [type, setType] = useState<AlertColor | null>(null);
|
|
37
|
+
const [msg, setMsg] = useState<string>('');
|
|
38
|
+
const [repeatCount, setRepeatCount] = useState(0);
|
|
39
|
+
const [expand, setExpand] = useState(false);
|
|
40
|
+
const [exitPromise, setExitPromise] = useState<Promise<void>>();
|
|
41
|
+
const [exitPromiseResolver, setExitPromiseResolver] = useState<() => void>();
|
|
42
|
+
|
|
43
|
+
const showAlert = useCallback(
|
|
44
|
+
(nextType: AlertColor, _nextMsg: string | Error) => {
|
|
45
|
+
// if the alert is identical to the previous alert, increment the
|
|
46
|
+
// `repeatCount` indicator.
|
|
47
|
+
const nextMsg = _nextMsg.toString();
|
|
48
|
+
if (nextType === type && nextMsg === msg) {
|
|
49
|
+
setRepeatCount(currCount => currCount + 1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === null) {
|
|
54
|
+
// if this alert is being shown for the first time, initialize the
|
|
55
|
+
// exitPromise so we can await it on `clear()`.
|
|
56
|
+
setExitPromise(
|
|
57
|
+
new Promise(res => {
|
|
58
|
+
setExitPromiseResolver(() => res);
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setType(nextType);
|
|
64
|
+
setMsg(nextMsg);
|
|
65
|
+
setRepeatCount(0);
|
|
66
|
+
setExpand(true);
|
|
67
|
+
},
|
|
68
|
+
[msg, type]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const alertJsx = useMemo(
|
|
72
|
+
() => (
|
|
73
|
+
<Collapse
|
|
74
|
+
in={expand}
|
|
75
|
+
onExited={() => {
|
|
76
|
+
exitPromiseResolver?.();
|
|
77
|
+
// only clear the alert after the Collapse exits, otherwise the alert
|
|
78
|
+
// disappears without any animation.
|
|
79
|
+
setType(null);
|
|
80
|
+
setMsg('');
|
|
81
|
+
setRepeatCount(0);
|
|
82
|
+
}}
|
|
83
|
+
timeout={200}
|
|
84
|
+
>
|
|
85
|
+
{type !== null && (
|
|
86
|
+
<Alert severity={type}>
|
|
87
|
+
{msg + (repeatCount ? ` (${repeatCount})` : '')}
|
|
88
|
+
</Alert>
|
|
89
|
+
)}
|
|
90
|
+
</Collapse>
|
|
91
|
+
),
|
|
92
|
+
[msg, repeatCount, type, expand, exitPromiseResolver]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const clearAlert = useCallback(() => {
|
|
96
|
+
setExpand(false);
|
|
97
|
+
return exitPromise;
|
|
98
|
+
}, [expand, exitPromise]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
show: showAlert,
|
|
102
|
+
jsx: alertJsx,
|
|
103
|
+
clear: clearAlert
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
8
|
+
import ReactDOM from 'react-dom';
|
|
9
|
+
|
|
10
|
+
import { CopyButton } from './copy-button';
|
|
11
|
+
import { MessageToolbar } from './toolbar';
|
|
12
|
+
|
|
13
|
+
const MD_MIME_TYPE = 'text/markdown';
|
|
14
|
+
const RENDERMIME_MD_CLASS = 'jp-chat-rendermime-markdown';
|
|
15
|
+
|
|
16
|
+
type RendermimeMarkdownProps = {
|
|
17
|
+
markdownStr: string;
|
|
18
|
+
rmRegistry: IRenderMimeRegistry;
|
|
19
|
+
appendContent?: boolean;
|
|
20
|
+
edit?: () => void;
|
|
21
|
+
delete?: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Takes \( and returns \\(. Escapes LaTeX delimeters by adding extra backslashes where needed for proper rendering by @jupyterlab/rendermime.
|
|
26
|
+
*/
|
|
27
|
+
function escapeLatexDelimiters(text: string) {
|
|
28
|
+
return text
|
|
29
|
+
.replace('\\(', '\\\\(')
|
|
30
|
+
.replace('\\)', '\\\\)')
|
|
31
|
+
.replace('\\[', '\\\\[')
|
|
32
|
+
.replace('\\]', '\\\\]');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element {
|
|
36
|
+
const appendContent = props.appendContent || false;
|
|
37
|
+
const [renderedContent, setRenderedContent] = useState<HTMLElement | null>(
|
|
38
|
+
null
|
|
39
|
+
);
|
|
40
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const renderContent = async () => {
|
|
44
|
+
const mdStr = escapeLatexDelimiters(props.markdownStr);
|
|
45
|
+
const model = props.rmRegistry.createModel({
|
|
46
|
+
data: { [MD_MIME_TYPE]: mdStr }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const renderer = props.rmRegistry.createRenderer(MD_MIME_TYPE);
|
|
50
|
+
await renderer.renderModel(model);
|
|
51
|
+
props.rmRegistry.latexTypesetter?.typeset(renderer.node);
|
|
52
|
+
|
|
53
|
+
// Attach CopyButton to each <pre> block
|
|
54
|
+
if (containerRef.current && renderer.node) {
|
|
55
|
+
const preBlocks = renderer.node.querySelectorAll('pre');
|
|
56
|
+
preBlocks.forEach(preBlock => {
|
|
57
|
+
const copyButtonContainer = document.createElement('div');
|
|
58
|
+
preBlock.parentNode?.insertBefore(
|
|
59
|
+
copyButtonContainer,
|
|
60
|
+
preBlock.nextSibling
|
|
61
|
+
);
|
|
62
|
+
ReactDOM.render(
|
|
63
|
+
<CopyButton value={preBlock.textContent || ''} />,
|
|
64
|
+
copyButtonContainer
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setRenderedContent(renderer.node);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
renderContent();
|
|
73
|
+
}, [props.markdownStr, props.rmRegistry]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div ref={containerRef} className={RENDERMIME_MD_CLASS}>
|
|
77
|
+
{renderedContent &&
|
|
78
|
+
(appendContent ? (
|
|
79
|
+
<div ref={node => node && node.appendChild(renderedContent)} />
|
|
80
|
+
) : (
|
|
81
|
+
<div ref={node => node && node.replaceChildren(renderedContent)} />
|
|
82
|
+
))}
|
|
83
|
+
<MessageToolbar edit={props.edit} delete={props.delete} />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const RendermimeMarkdown = React.memo(RendermimeMarkdownBase);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useMemo } from 'react';
|
|
7
|
+
import { Box, SxProps, Theme } from '@mui/material';
|
|
8
|
+
|
|
9
|
+
type ScrollContainerProps = {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
sx?: SxProps<Theme>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Component that handles intelligent scrolling.
|
|
16
|
+
*
|
|
17
|
+
* - If viewport is at the bottom of the overflow container, appending new
|
|
18
|
+
* children keeps the viewport on the bottom of the overflow container.
|
|
19
|
+
*
|
|
20
|
+
* - If viewport is in the middle of the overflow container, appending new
|
|
21
|
+
* children leaves the viewport unaffected.
|
|
22
|
+
*
|
|
23
|
+
* Currently only works for Chrome and Firefox due to reliance on
|
|
24
|
+
* `overflow-anchor`.
|
|
25
|
+
*
|
|
26
|
+
* **References**
|
|
27
|
+
* - https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
|
|
28
|
+
*/
|
|
29
|
+
export function ScrollContainer(props: ScrollContainerProps): JSX.Element {
|
|
30
|
+
const id = useMemo(
|
|
31
|
+
() => 'jupyter-chat-scroll-container-' + Date.now().toString(),
|
|
32
|
+
[]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Effect: Scroll the container to the bottom as soon as it is visible.
|
|
37
|
+
*/
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const el = document.querySelector<HTMLElement>(`#${id}`);
|
|
40
|
+
if (!el) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const observer = new IntersectionObserver(
|
|
45
|
+
entries => {
|
|
46
|
+
entries.forEach(entry => {
|
|
47
|
+
if (entry.isIntersecting) {
|
|
48
|
+
el.scroll({ top: 999999999 });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
{ threshold: 1.0 }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
observer.observe(el);
|
|
56
|
+
return () => observer.disconnect();
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box
|
|
61
|
+
id={id}
|
|
62
|
+
sx={{
|
|
63
|
+
overflowY: 'scroll',
|
|
64
|
+
'& *': {
|
|
65
|
+
overflowAnchor: 'none'
|
|
66
|
+
},
|
|
67
|
+
...props.sx
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<Box sx={{ minHeight: '100.01%' }}>{props.children}</Box>
|
|
71
|
+
<Box sx={{ overflowAnchor: 'auto', height: '1px' }} />
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ToolbarButtonComponent,
|
|
8
|
+
deleteIcon,
|
|
9
|
+
editIcon
|
|
10
|
+
} from '@jupyterlab/ui-components';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
const TOOLBAR_CLASS = 'jp-chat-toolbar';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The toolbar attached to a message.
|
|
17
|
+
*/
|
|
18
|
+
export function MessageToolbar(props: MessageToolbar.IProps): JSX.Element {
|
|
19
|
+
const buttons: JSX.Element[] = [];
|
|
20
|
+
|
|
21
|
+
if (props.edit !== undefined) {
|
|
22
|
+
const editButton = ToolbarButtonComponent({
|
|
23
|
+
icon: editIcon,
|
|
24
|
+
onClick: props.edit,
|
|
25
|
+
tooltip: 'Edit'
|
|
26
|
+
});
|
|
27
|
+
buttons.push(editButton);
|
|
28
|
+
}
|
|
29
|
+
if (props.delete !== undefined) {
|
|
30
|
+
const deleteButton = ToolbarButtonComponent({
|
|
31
|
+
icon: deleteIcon,
|
|
32
|
+
onClick: props.delete,
|
|
33
|
+
tooltip: 'Delete'
|
|
34
|
+
});
|
|
35
|
+
buttons.push(deleteButton);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={TOOLBAR_CLASS}>
|
|
40
|
+
{buttons.map(toolbarButton => toolbarButton)}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export namespace MessageToolbar {
|
|
46
|
+
export interface IProps {
|
|
47
|
+
edit?: () => void;
|
|
48
|
+
delete?: () => void;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// This file is based on iconimports.ts in @jupyterlab/ui-components, but is manually generated.
|
|
7
|
+
|
|
8
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
9
|
+
|
|
10
|
+
import chatSvgStr from '../style/icons/chat.svg';
|
|
11
|
+
|
|
12
|
+
export const chatIcon = new LabIcon({
|
|
13
|
+
name: 'jupyter-chat::chat',
|
|
14
|
+
svgstr: chatSvgStr
|
|
15
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './icons';
|
|
7
|
+
export * from './model';
|
|
8
|
+
export * from './types';
|
|
9
|
+
export * from './widgets/chat-error';
|
|
10
|
+
export * from './widgets/chat-sidebar';
|
|
11
|
+
export * from './widgets/chat-widget';
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDisposable } from '@lumino/disposable';
|
|
7
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
IChatHistory,
|
|
11
|
+
INewMessage,
|
|
12
|
+
IChatMessage,
|
|
13
|
+
IConfig,
|
|
14
|
+
IUser
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The chat model interface.
|
|
19
|
+
*/
|
|
20
|
+
export interface IChatModel extends IDisposable {
|
|
21
|
+
/**
|
|
22
|
+
* The chat model ID.
|
|
23
|
+
*/
|
|
24
|
+
id: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The configuration for the chat panel.
|
|
28
|
+
*/
|
|
29
|
+
config: IConfig;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The user connected to the chat panel.
|
|
33
|
+
*/
|
|
34
|
+
readonly user?: IUser;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The chat messages list.
|
|
38
|
+
*/
|
|
39
|
+
readonly messages: IChatMessage[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The signal emitted when the messages list is updated.
|
|
43
|
+
*/
|
|
44
|
+
readonly messagesUpdated: ISignal<IChatModel, void>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send a message, to be defined depending on the chosen technology.
|
|
48
|
+
* Default to no-op.
|
|
49
|
+
*
|
|
50
|
+
* @param message - the message to send.
|
|
51
|
+
* @returns whether the message has been sent or not, or nothing if not needed.
|
|
52
|
+
*/
|
|
53
|
+
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional, to update a message from the chat panel.
|
|
57
|
+
*
|
|
58
|
+
* @param id - the unique ID of the message.
|
|
59
|
+
* @param message - the updated message.
|
|
60
|
+
*/
|
|
61
|
+
updateMessage?(
|
|
62
|
+
id: string,
|
|
63
|
+
message: IChatMessage
|
|
64
|
+
): Promise<boolean | void> | boolean | void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Optional, to delete a message from the chat.
|
|
68
|
+
*
|
|
69
|
+
* @param id - the unique ID of the message.
|
|
70
|
+
*/
|
|
71
|
+
deleteMessage?(id: string): Promise<boolean | void> | boolean | void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional, to get messages history.
|
|
75
|
+
*/
|
|
76
|
+
getHistory?(): Promise<IChatHistory>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Dispose the chat model.
|
|
80
|
+
*/
|
|
81
|
+
dispose(): void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the chat handler is disposed.
|
|
85
|
+
*/
|
|
86
|
+
isDisposed: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Function to call when a message is received.
|
|
90
|
+
*
|
|
91
|
+
* @param message - the message with user information and body.
|
|
92
|
+
*/
|
|
93
|
+
messageAdded(message: IChatMessage): void;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Function called when messages are inserted.
|
|
97
|
+
*
|
|
98
|
+
* @param index - the index of the first message of the list.
|
|
99
|
+
* @param messages - the messages list.
|
|
100
|
+
*/
|
|
101
|
+
messagesInserted(index: number, messages: IChatMessage[]): void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Function called when messages are deleted.
|
|
105
|
+
*
|
|
106
|
+
* @param index - the index of the first message to delete.
|
|
107
|
+
* @param count - the number of messages to delete.
|
|
108
|
+
*/
|
|
109
|
+
messagesDeleted(index: number, count: number): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The default chat model implementation.
|
|
114
|
+
* It is not able to send or update a message by itself, since it depends on the
|
|
115
|
+
* chosen technology.
|
|
116
|
+
*/
|
|
117
|
+
export class ChatModel implements IChatModel {
|
|
118
|
+
/**
|
|
119
|
+
* Create a new chat model.
|
|
120
|
+
*/
|
|
121
|
+
constructor(options: ChatModel.IOptions = {}) {
|
|
122
|
+
this._config = options.config ?? {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The chat messages list.
|
|
127
|
+
*/
|
|
128
|
+
get messages(): IChatMessage[] {
|
|
129
|
+
return this._messages;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The chat model ID.
|
|
134
|
+
*/
|
|
135
|
+
get id(): string {
|
|
136
|
+
return this._id;
|
|
137
|
+
}
|
|
138
|
+
set id(value: string) {
|
|
139
|
+
this._id = value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The chat settings.
|
|
144
|
+
*/
|
|
145
|
+
get config(): IConfig {
|
|
146
|
+
return this._config;
|
|
147
|
+
}
|
|
148
|
+
set config(value: Partial<IConfig>) {
|
|
149
|
+
this._config = { ...this._config, ...value };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The signal emitted when the messages list is updated.
|
|
154
|
+
*/
|
|
155
|
+
get messagesUpdated(): ISignal<IChatModel, void> {
|
|
156
|
+
return this._messagesUpdated;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Send a message, to be defined depending on the chosen technology.
|
|
161
|
+
* Default to no-op.
|
|
162
|
+
*
|
|
163
|
+
* @param message - the message to send.
|
|
164
|
+
* @returns whether the message has been sent or not.
|
|
165
|
+
*/
|
|
166
|
+
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Optional, to update a message from the chat panel.
|
|
170
|
+
*
|
|
171
|
+
* @param id - the unique ID of the message.
|
|
172
|
+
* @param message - the message to update.
|
|
173
|
+
*/
|
|
174
|
+
updateMessage?(
|
|
175
|
+
id: string,
|
|
176
|
+
message: INewMessage
|
|
177
|
+
): Promise<boolean | void> | boolean | void;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Dispose the chat model.
|
|
181
|
+
*/
|
|
182
|
+
dispose(): void {
|
|
183
|
+
if (this.isDisposed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this._isDisposed = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Whether the chat handler is disposed.
|
|
191
|
+
*/
|
|
192
|
+
get isDisposed(): boolean {
|
|
193
|
+
return this._isDisposed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A function called before transferring the message to the panel(s).
|
|
198
|
+
* Can be useful if some actions are required on the message.
|
|
199
|
+
*/
|
|
200
|
+
protected formatChatMessage(message: IChatMessage): IChatMessage {
|
|
201
|
+
return message;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Function to call when a message is received.
|
|
206
|
+
*
|
|
207
|
+
* @param message - the message with user information and body.
|
|
208
|
+
*/
|
|
209
|
+
messageAdded(message: IChatMessage): void {
|
|
210
|
+
const messageIndex = this._messages.findIndex(msg => msg.id === message.id);
|
|
211
|
+
if (messageIndex > -1) {
|
|
212
|
+
// The message is an update of an existing one.
|
|
213
|
+
// Let's remove it to avoid position conflict if timestamp has changed.
|
|
214
|
+
this._messages.splice(messageIndex, 1);
|
|
215
|
+
}
|
|
216
|
+
// Find the first message that should be after this one.
|
|
217
|
+
let nextMsgIndex = this._messages.findIndex(msg => msg.time > message.time);
|
|
218
|
+
if (nextMsgIndex === -1) {
|
|
219
|
+
// There is no message after this one, so let's insert the message at the end.
|
|
220
|
+
nextMsgIndex = this._messages.length;
|
|
221
|
+
}
|
|
222
|
+
// Insert the message.
|
|
223
|
+
this.messagesInserted(nextMsgIndex, [message]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Function called when messages are inserted.
|
|
228
|
+
*
|
|
229
|
+
* @param index - the index of the first message of the list.
|
|
230
|
+
* @param messages - the messages list.
|
|
231
|
+
*/
|
|
232
|
+
messagesInserted(index: number, messages: IChatMessage[]): void {
|
|
233
|
+
const formattedMessages: IChatMessage[] = [];
|
|
234
|
+
messages.forEach(message => {
|
|
235
|
+
formattedMessages.push(this.formatChatMessage(message));
|
|
236
|
+
});
|
|
237
|
+
this._messages.splice(index, 0, ...formattedMessages);
|
|
238
|
+
this._messagesUpdated.emit();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Function called when messages are deleted.
|
|
243
|
+
*
|
|
244
|
+
* @param index - the index of the first message to delete.
|
|
245
|
+
* @param count - the number of messages to delete.
|
|
246
|
+
*/
|
|
247
|
+
messagesDeleted(index: number, count: number): void {
|
|
248
|
+
this._messages.splice(index, count);
|
|
249
|
+
this._messagesUpdated.emit();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private _messages: IChatMessage[] = [];
|
|
253
|
+
private _id: string = '';
|
|
254
|
+
private _config: IConfig;
|
|
255
|
+
private _isDisposed = false;
|
|
256
|
+
private _messagesUpdated = new Signal<IChatModel, void>(this);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The chat model namespace.
|
|
261
|
+
*/
|
|
262
|
+
export namespace ChatModel {
|
|
263
|
+
/**
|
|
264
|
+
* The instantiation options for a ChatModel.
|
|
265
|
+
*/
|
|
266
|
+
export interface IOptions {
|
|
267
|
+
/**
|
|
268
|
+
* Initial config for the chat widget.
|
|
269
|
+
*/
|
|
270
|
+
config?: IConfig;
|
|
271
|
+
}
|
|
272
|
+
}
|