@jupyter/chat 0.13.0 → 0.15.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/active-cell-manager.d.ts +2 -0
- package/lib/active-cell-manager.js +7 -2
- package/lib/components/avatar.d.ts +20 -0
- package/lib/components/avatar.js +29 -0
- package/lib/components/chat.d.ts +1 -3
- package/lib/components/chat.js +2 -3
- package/lib/components/index.d.ts +2 -3
- package/lib/components/index.js +2 -3
- package/lib/components/input/buttons/send-button.js +15 -5
- package/lib/components/{chat-input.d.ts → input/chat-input.d.ts} +3 -3
- package/lib/components/{chat-input.js → input/chat-input.js} +8 -5
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/toolbar-registry.d.ts +6 -0
- package/lib/components/input/use-chat-commands.d.ts +1 -1
- package/lib/components/input/use-chat-commands.js +32 -13
- package/lib/components/messages/footer.d.ts +2 -2
- package/lib/components/messages/footer.js +1 -1
- package/lib/components/messages/header.d.ts +16 -0
- package/lib/components/messages/header.js +85 -0
- package/lib/components/messages/index.d.ts +9 -0
- package/lib/components/messages/index.js +13 -0
- package/lib/components/messages/message-renderer.js +1 -1
- package/lib/components/messages/message.d.ts +21 -0
- package/lib/components/messages/message.js +102 -0
- package/lib/components/messages/messages.d.ts +38 -0
- package/lib/components/messages/messages.js +139 -0
- package/lib/components/messages/navigation.d.ts +20 -0
- package/lib/components/messages/navigation.js +98 -0
- package/lib/components/messages/writers.d.ts +16 -0
- package/lib/components/messages/writers.js +39 -0
- package/lib/context.d.ts +1 -1
- package/lib/index.d.ts +2 -6
- package/lib/index.js +2 -6
- package/lib/input-model.js +30 -3
- package/lib/{registry.d.ts → registers/attachment-openers.d.ts} +1 -1
- package/lib/registers/chat-commands.d.ts +108 -0
- package/lib/{chat-commands/registry.js → registers/chat-commands.js} +8 -8
- package/lib/{footers/registry.d.ts → registers/footers.d.ts} +30 -5
- package/lib/registers/index.d.ts +3 -0
- package/lib/{footers → registers}/index.js +3 -2
- package/lib/selection-watcher.d.ts +11 -1
- package/lib/selection-watcher.js +10 -4
- package/lib/types.d.ts +7 -2
- package/lib/utils.js +8 -6
- package/lib/widgets/index.d.ts +3 -0
- package/lib/{chat-commands → widgets}/index.js +3 -2
- package/package.json +3 -1
- package/src/active-cell-manager.ts +10 -1
- package/src/components/avatar.tsx +68 -0
- package/src/components/chat.tsx +11 -6
- package/src/components/index.ts +2 -3
- package/src/components/input/buttons/send-button.tsx +17 -5
- package/src/components/{chat-input.tsx → input/chat-input.tsx} +13 -8
- package/src/components/input/index.ts +1 -0
- package/src/components/input/toolbar-registry.tsx +6 -0
- package/src/components/input/use-chat-commands.tsx +39 -16
- package/src/components/messages/footer.tsx +5 -2
- package/src/components/messages/header.tsx +133 -0
- package/src/components/messages/index.ts +14 -0
- package/src/components/messages/message-renderer.tsx +1 -1
- package/src/components/messages/message.tsx +156 -0
- package/src/components/messages/messages.tsx +218 -0
- package/src/components/messages/navigation.tsx +167 -0
- package/src/components/messages/welcome.tsx +1 -0
- package/src/components/messages/writers.tsx +81 -0
- package/src/context.ts +1 -1
- package/src/index.ts +2 -6
- package/src/input-model.ts +33 -4
- package/src/{registry.ts → registers/attachment-openers.ts} +2 -1
- package/src/registers/chat-commands.ts +142 -0
- package/src/{footers/registry.ts → registers/footers.ts} +35 -8
- package/src/{footers → registers}/index.ts +3 -2
- package/src/selection-watcher.ts +28 -5
- package/src/types.ts +7 -2
- package/src/utils.ts +8 -6
- package/src/{chat-commands → widgets}/index.ts +3 -2
- package/style/chat.css +82 -0
- package/lib/chat-commands/index.d.ts +0 -2
- package/lib/chat-commands/registry.d.ts +0 -28
- package/lib/chat-commands/types.d.ts +0 -52
- package/lib/chat-commands/types.js +0 -5
- package/lib/components/chat-messages.d.ts +0 -119
- package/lib/components/chat-messages.js +0 -446
- package/lib/footers/index.d.ts +0 -2
- package/lib/footers/types.d.ts +0 -26
- package/lib/footers/types.js +0 -5
- package/src/chat-commands/registry.ts +0 -60
- package/src/chat-commands/types.ts +0 -67
- package/src/components/chat-messages.tsx +0 -739
- package/src/footers/types.ts +0 -33
- package/lib/components/{toolbar.d.ts → messages/toolbar.d.ts} +0 -0
- package/lib/components/{toolbar.js → messages/toolbar.js} +0 -0
- package/lib/{registry.js → registers/attachment-openers.js} +0 -0
- package/lib/{footers/registry.js → registers/footers.js} +4 -4
- /package/src/components/{toolbar.tsx → messages/toolbar.tsx} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Button } from '@jupyter/react-components';
|
|
7
|
+
import {
|
|
8
|
+
LabIcon,
|
|
9
|
+
caretDownEmptyIcon,
|
|
10
|
+
classes
|
|
11
|
+
} from '@jupyterlab/ui-components';
|
|
12
|
+
import React, { useEffect, useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import { BaseMessageProps } from './messages';
|
|
15
|
+
import { IChatModel } from '../../model';
|
|
16
|
+
|
|
17
|
+
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation';
|
|
18
|
+
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread';
|
|
19
|
+
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top';
|
|
20
|
+
const NAVIGATION_BOTTOM_CLASS = 'jp-chat-navigation-bottom';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The navigation component props.
|
|
24
|
+
*/
|
|
25
|
+
type NavigationProps = BaseMessageProps & {
|
|
26
|
+
/**
|
|
27
|
+
* The reference to the messages container.
|
|
28
|
+
*/
|
|
29
|
+
refMsgBox: React.RefObject<HTMLDivElement>;
|
|
30
|
+
/**
|
|
31
|
+
* Whether all the messages has been rendered once on first display.
|
|
32
|
+
*/
|
|
33
|
+
allRendered: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The navigation component, to navigate to unread messages.
|
|
38
|
+
*/
|
|
39
|
+
export function Navigation(props: NavigationProps): JSX.Element {
|
|
40
|
+
const { model } = props;
|
|
41
|
+
const [lastInViewport, setLastInViewport] = useState<boolean>(true);
|
|
42
|
+
const [unreadBefore, setUnreadBefore] = useState<number | null>(null);
|
|
43
|
+
const [unreadAfter, setUnreadAfter] = useState<number | null>(null);
|
|
44
|
+
|
|
45
|
+
const gotoMessage = (msgIdx: number, alignToTop: boolean = true) => {
|
|
46
|
+
props.refMsgBox.current?.children.item(msgIdx)?.scrollIntoView(alignToTop);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Listen for change in unread messages, and find the first unread message before or
|
|
50
|
+
// after the current viewport, to display navigation buttons.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Do not attempt to display navigation until messages are rendered, it can lead to
|
|
53
|
+
// wrong assumption, because more messages are in the viewport before they are
|
|
54
|
+
// rendered.
|
|
55
|
+
if (!props.allRendered) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const unreadChanged = (model: IChatModel, unreadIndexes: number[]) => {
|
|
60
|
+
const viewport = model.messagesInViewport;
|
|
61
|
+
if (!viewport) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Initialize the next values with the current values if there still relevant.
|
|
66
|
+
let before =
|
|
67
|
+
unreadBefore !== null &&
|
|
68
|
+
unreadIndexes.includes(unreadBefore) &&
|
|
69
|
+
unreadBefore < Math.min(...viewport)
|
|
70
|
+
? unreadBefore
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
let after =
|
|
74
|
+
unreadAfter !== null &&
|
|
75
|
+
unreadIndexes.includes(unreadAfter) &&
|
|
76
|
+
unreadAfter > Math.max(...viewport)
|
|
77
|
+
? unreadAfter
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
unreadIndexes.forEach(unread => {
|
|
81
|
+
if (viewport?.includes(unread)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (unread < (before ?? Math.min(...viewport))) {
|
|
85
|
+
before = unread;
|
|
86
|
+
} else if (
|
|
87
|
+
unread > Math.max(...viewport) &&
|
|
88
|
+
unread < (after ?? model.messages.length)
|
|
89
|
+
) {
|
|
90
|
+
after = unread;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
setUnreadBefore(before);
|
|
95
|
+
setUnreadAfter(after);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
model.unreadChanged?.connect(unreadChanged);
|
|
99
|
+
|
|
100
|
+
unreadChanged(model, model.unreadMessages);
|
|
101
|
+
|
|
102
|
+
// Move to the last the message after all the messages have been first rendered.
|
|
103
|
+
gotoMessage(model.messages.length - 1, false);
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
model.unreadChanged?.disconnect(unreadChanged);
|
|
107
|
+
};
|
|
108
|
+
}, [model, props.allRendered]);
|
|
109
|
+
|
|
110
|
+
// Listen for change in the viewport, to add a navigation button if the last is not
|
|
111
|
+
// in viewport.
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const viewportChanged = (model: IChatModel, viewport: number[]) => {
|
|
114
|
+
setLastInViewport(
|
|
115
|
+
model.messages.length === 0 ||
|
|
116
|
+
viewport.includes(model.messages.length - 1)
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
model.viewportChanged?.connect(viewportChanged);
|
|
121
|
+
|
|
122
|
+
viewportChanged(model, model.messagesInViewport ?? []);
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
model.viewportChanged?.disconnect(viewportChanged);
|
|
126
|
+
};
|
|
127
|
+
}, [model]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
{unreadBefore !== null && (
|
|
132
|
+
<Button
|
|
133
|
+
className={`${NAVIGATION_BUTTON_CLASS} ${NAVIGATION_UNREAD_CLASS} ${NAVIGATION_TOP_CLASS}`}
|
|
134
|
+
onClick={() => gotoMessage!(unreadBefore)}
|
|
135
|
+
title={'Go to unread messages'}
|
|
136
|
+
>
|
|
137
|
+
<LabIcon.resolveReact
|
|
138
|
+
display={'flex'}
|
|
139
|
+
icon={caretDownEmptyIcon}
|
|
140
|
+
iconClass={classes('jp-Icon')}
|
|
141
|
+
/>
|
|
142
|
+
</Button>
|
|
143
|
+
)}
|
|
144
|
+
{(unreadAfter !== null || !lastInViewport) && (
|
|
145
|
+
<Button
|
|
146
|
+
className={`${NAVIGATION_BUTTON_CLASS} ${unreadAfter !== null ? NAVIGATION_UNREAD_CLASS : ''} ${NAVIGATION_BOTTOM_CLASS}`}
|
|
147
|
+
onClick={
|
|
148
|
+
unreadAfter === null
|
|
149
|
+
? () => gotoMessage(model.messages.length - 1, false)
|
|
150
|
+
: () => gotoMessage(unreadAfter)
|
|
151
|
+
}
|
|
152
|
+
title={
|
|
153
|
+
unreadAfter !== null
|
|
154
|
+
? 'Go to unread messages'
|
|
155
|
+
: 'Go to last message'
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<LabIcon.resolveReact
|
|
159
|
+
display={'flex'}
|
|
160
|
+
icon={caretDownEmptyIcon}
|
|
161
|
+
iconClass={classes('jp-Icon')}
|
|
162
|
+
/>
|
|
163
|
+
</Button>
|
|
164
|
+
)}
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Box, Typography } from '@mui/material';
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Avatar } from '../avatar';
|
|
10
|
+
import { IUser } from '../../types';
|
|
11
|
+
|
|
12
|
+
const WRITERS_CLASS = 'jp-chat-writers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The writers component props.
|
|
16
|
+
*/
|
|
17
|
+
type writersProps = {
|
|
18
|
+
/**
|
|
19
|
+
* The list of users currently writing.
|
|
20
|
+
*/
|
|
21
|
+
writers: IUser[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Animated typing indicator component
|
|
26
|
+
*/
|
|
27
|
+
const TypingIndicator = (): JSX.Element => (
|
|
28
|
+
<Box className="jp-chat-typing-indicator">
|
|
29
|
+
<span className="jp-chat-typing-dot"></span>
|
|
30
|
+
<span className="jp-chat-typing-dot"></span>
|
|
31
|
+
<span className="jp-chat-typing-dot"></span>
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The writers component, displaying the current writers.
|
|
37
|
+
*/
|
|
38
|
+
export function WritingUsersList(props: writersProps): JSX.Element | null {
|
|
39
|
+
const { writers } = props;
|
|
40
|
+
|
|
41
|
+
// Don't render if no writers
|
|
42
|
+
if (writers.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const writersText = writers.length > 1 ? ' are writing' : ' is writing';
|
|
47
|
+
|
|
48
|
+
const writingUsers: JSX.Element[] = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
writers.map((writer, index) => (
|
|
51
|
+
<Box key={writer.username || index} className="jp-chat-writer-item">
|
|
52
|
+
<Avatar user={writer} small />
|
|
53
|
+
<Typography variant="body2" className="jp-chat-writer-name">
|
|
54
|
+
{writer.display_name ??
|
|
55
|
+
writer.name ??
|
|
56
|
+
(writer.username || 'User undefined')}
|
|
57
|
+
</Typography>
|
|
58
|
+
{index < writers.length - 1 && (
|
|
59
|
+
<Typography variant="body2" className="jp-chat-writer-separator">
|
|
60
|
+
{index < writers.length - 2 ? ', ' : ' and '}
|
|
61
|
+
</Typography>
|
|
62
|
+
)}
|
|
63
|
+
</Box>
|
|
64
|
+
)),
|
|
65
|
+
[writers]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Box className={`${WRITERS_CLASS}`}>
|
|
70
|
+
<Box className="jp-chat-writers-content">
|
|
71
|
+
{writingUsers}
|
|
72
|
+
<Box className="jp-chat-writing-status">
|
|
73
|
+
<Typography variant="body2" className="jp-chat-writing-text">
|
|
74
|
+
{writersText}
|
|
75
|
+
</Typography>
|
|
76
|
+
<TypingIndicator />
|
|
77
|
+
</Box>
|
|
78
|
+
</Box>
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
import { createContext } from 'react';
|
|
6
|
-
import { IAttachmentOpenerRegistry } from './
|
|
6
|
+
import { IAttachmentOpenerRegistry } from './registers';
|
|
7
7
|
|
|
8
8
|
export const AttachmentOpenerContext = createContext<
|
|
9
9
|
IAttachmentOpenerRegistry | undefined
|
package/src/index.ts
CHANGED
|
@@ -4,16 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export * from './active-cell-manager';
|
|
7
|
-
export * from './chat-commands';
|
|
8
7
|
export * from './components';
|
|
9
|
-
export * from './footers';
|
|
10
8
|
export * from './icons';
|
|
11
9
|
export * from './input-model';
|
|
12
10
|
export * from './markdown-renderer';
|
|
13
11
|
export * from './model';
|
|
14
|
-
export * from './
|
|
12
|
+
export * from './registers';
|
|
15
13
|
export * from './selection-watcher';
|
|
16
14
|
export * from './types';
|
|
17
|
-
export * from './widgets
|
|
18
|
-
export * from './widgets/chat-sidebar';
|
|
19
|
-
export * from './widgets/chat-widget';
|
|
15
|
+
export * from './widgets';
|
package/src/input-model.ts
CHANGED
|
@@ -11,8 +11,6 @@ import { ISelectionWatcher } from './selection-watcher';
|
|
|
11
11
|
import { IChatContext } from './model';
|
|
12
12
|
import { IAttachment, IUser } from './types';
|
|
13
13
|
|
|
14
|
-
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* The chat input interface.
|
|
18
16
|
*/
|
|
@@ -525,6 +523,18 @@ export namespace InputModel {
|
|
|
525
523
|
}
|
|
526
524
|
|
|
527
525
|
namespace Private {
|
|
526
|
+
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Returns the start index (inclusive) & end index (exclusive) that contain
|
|
530
|
+
* the current word. The start & end index can be passed to `String.slice()`
|
|
531
|
+
* to extract the current word. The returned range never includes any
|
|
532
|
+
* whitespace character unless it is escaped by a backslash `\`.
|
|
533
|
+
*
|
|
534
|
+
* NOTE: the escape sequence handling here is naive and non-recursive. This
|
|
535
|
+
* function considers the space in "`\\ `" as escaped, even though "`\\ `"
|
|
536
|
+
* defines a backslash followed by an _unescaped_ space in most languages.
|
|
537
|
+
*/
|
|
528
538
|
export function getCurrentWordBoundaries(
|
|
529
539
|
input: string,
|
|
530
540
|
cursorIndex: number
|
|
@@ -533,11 +543,30 @@ namespace Private {
|
|
|
533
543
|
let end = cursorIndex;
|
|
534
544
|
const n = input.length;
|
|
535
545
|
|
|
536
|
-
while (
|
|
546
|
+
while (
|
|
547
|
+
// terminate when `input[start - 1]` is whitespace
|
|
548
|
+
// i.e. `input[start]` is never whitespace after exiting
|
|
549
|
+
(start - 1 >= 0 && !WHITESPACE.has(input[start - 1])) ||
|
|
550
|
+
// unless it is preceded by a backslash
|
|
551
|
+
(start - 2 >= 0 &&
|
|
552
|
+
input[start - 2] === '\\' &&
|
|
553
|
+
WHITESPACE.has(input[start - 1]))
|
|
554
|
+
) {
|
|
537
555
|
start--;
|
|
538
556
|
}
|
|
539
557
|
|
|
540
|
-
|
|
558
|
+
// `end` is an exclusive index unlike `start`, hence the different `while`
|
|
559
|
+
// condition here
|
|
560
|
+
while (
|
|
561
|
+
// terminate when `input[end]` is whitespace
|
|
562
|
+
// i.e. `input[end]` may be whitespace after exiting
|
|
563
|
+
(end < n && !WHITESPACE.has(input[end])) ||
|
|
564
|
+
// unless it is preceded by a backslash
|
|
565
|
+
(end < n &&
|
|
566
|
+
end - 1 >= 0 &&
|
|
567
|
+
input[end - 1] === '\\' &&
|
|
568
|
+
WHITESPACE.has(input[end]))
|
|
569
|
+
) {
|
|
541
570
|
end++;
|
|
542
571
|
}
|
|
543
572
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
|
|
5
6
|
import { Token } from '@lumino/coreutils';
|
|
6
|
-
import { IAttachment } from '
|
|
7
|
+
import { IAttachment } from '../types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The token for the attachments opener registry, which can be provided by an extension
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
7
|
+
import { Token } from '@lumino/coreutils';
|
|
8
|
+
import { IInputModel } from '../input-model';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The token for the chat command registry, which can be provided by an extension
|
|
12
|
+
* using @jupyter/chat package.
|
|
13
|
+
*/
|
|
14
|
+
export const IChatCommandRegistry = new Token<IChatCommandRegistry>(
|
|
15
|
+
'@jupyter/chat:IChatCommandRegistry'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface of a chat command registry, which tracks a list of chat command
|
|
20
|
+
* providers. Providers provide a list of commands given a user's partial input,
|
|
21
|
+
* and define how commands are handled when accepted in the chat commands menu.
|
|
22
|
+
*/
|
|
23
|
+
export interface IChatCommandRegistry {
|
|
24
|
+
/**
|
|
25
|
+
* Adds a chat command provider to the registry.
|
|
26
|
+
*/
|
|
27
|
+
addProvider(provider: IChatCommandProvider): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lists all chat command providers previously added via `addProvider()`.
|
|
31
|
+
*/
|
|
32
|
+
getProviders(): IChatCommandProvider[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Calls `onSubmit()` on each command provider in serial. Each command
|
|
36
|
+
* provider's `onSubmit()` method is responsible for checking the entire input
|
|
37
|
+
* for command calls and handling them accordingly.
|
|
38
|
+
*
|
|
39
|
+
* This method is called by the application after the user presses the "Send"
|
|
40
|
+
* button but before the message is sent to server.
|
|
41
|
+
*/
|
|
42
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ChatCommand = {
|
|
46
|
+
/**
|
|
47
|
+
* The name of the command. This defines what the user should type in the
|
|
48
|
+
* input to have the command appear in the chat commands menu.
|
|
49
|
+
*/
|
|
50
|
+
name: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ID of the provider the command originated from.
|
|
54
|
+
*/
|
|
55
|
+
providerId: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* If set, this will be rendered as the icon for the command in the chat
|
|
59
|
+
* commands menu. Jupyter Chat will choose a default if this is unset.
|
|
60
|
+
*/
|
|
61
|
+
icon?: LabIcon | JSX.Element | string | null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* If set, this will be rendered as the description for the command in the
|
|
65
|
+
* chat commands menu. Jupyter Chat will choose a default if this is unset.
|
|
66
|
+
*/
|
|
67
|
+
description?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* If set, Jupyter Chat will replace the current word with this string immediately when
|
|
71
|
+
* the command is accepted from the chat commands menu or the current word
|
|
72
|
+
* matches the command's `name` exactly.
|
|
73
|
+
*
|
|
74
|
+
* This is generally used by "shortcut command" providers, e.g. the emoji
|
|
75
|
+
* command provider.
|
|
76
|
+
*/
|
|
77
|
+
replaceWith?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Specifies whether the application should add a space ' ' after the command
|
|
81
|
+
* is accepted from the menu. This should be set to `true` if the command that
|
|
82
|
+
* replaces the current word needs to be handled on submit, and the command is
|
|
83
|
+
* valid on its own.
|
|
84
|
+
*
|
|
85
|
+
* Defaults to `false`.
|
|
86
|
+
*/
|
|
87
|
+
spaceOnAccept?: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Interface of a command provider.
|
|
92
|
+
*/
|
|
93
|
+
export interface IChatCommandProvider {
|
|
94
|
+
/**
|
|
95
|
+
* ID of this command provider.
|
|
96
|
+
*/
|
|
97
|
+
id: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A method that should return the list of valid chat commands whose names
|
|
101
|
+
* complete the current word.
|
|
102
|
+
*
|
|
103
|
+
* The current word should be accessed from `inputModel.currentWord`.
|
|
104
|
+
*/
|
|
105
|
+
listCommandCompletions(inputModel: IInputModel): Promise<ChatCommand[]>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A method that should identify and handle *all* command calls within a
|
|
109
|
+
* message that the user intends to submit. This method is called after a user
|
|
110
|
+
* presses the "Send" button but before the message is sent to the server.
|
|
111
|
+
*
|
|
112
|
+
* The entire message should be read from `inputModel.value`. This method may
|
|
113
|
+
* modify the new message before submission by setting `inputModel.value` or
|
|
114
|
+
* by calling other methods available on `inputModel`.
|
|
115
|
+
*/
|
|
116
|
+
onSubmit(inputModel: IInputModel): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Default chat command registry implementation.
|
|
121
|
+
*/
|
|
122
|
+
export class ChatCommandRegistry implements IChatCommandRegistry {
|
|
123
|
+
constructor() {
|
|
124
|
+
this._providers = new Map<string, IChatCommandProvider>();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
addProvider(provider: IChatCommandProvider): void {
|
|
128
|
+
this._providers.set(provider.id, provider);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getProviders(): IChatCommandProvider[] {
|
|
132
|
+
return Array.from(this._providers.values());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async onSubmit(inputModel: IInputModel) {
|
|
136
|
+
for (const provider of this._providers.values()) {
|
|
137
|
+
await provider.onSubmit(inputModel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _providers: Map<string, IChatCommandProvider>;
|
|
142
|
+
}
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Token } from '@lumino/coreutils';
|
|
7
|
-
import {
|
|
7
|
+
import { IChatModel } from '../model';
|
|
8
|
+
import { IChatMessage } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The token providing the chat footer registry.
|
|
12
|
+
*/
|
|
13
|
+
export const IMessageFooterRegistry = new Token<IMessageFooterRegistry>(
|
|
14
|
+
'@jupyter/chat:ChatFooterRegistry'
|
|
15
|
+
);
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* The interface of a registry to provide chat footer.
|
|
@@ -22,6 +30,32 @@ export interface IMessageFooterRegistry {
|
|
|
22
30
|
addSection(section: MessageFooterSection): void;
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The props sent passed to each `MessageFooterSection` React component.
|
|
35
|
+
*/
|
|
36
|
+
export type MessageFooterSectionProps = {
|
|
37
|
+
model: IChatModel;
|
|
38
|
+
message: IChatMessage;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A message footer section which can be added to the footer registry.
|
|
43
|
+
*/
|
|
44
|
+
export type MessageFooterSection = {
|
|
45
|
+
component: React.FC<MessageFooterSectionProps>;
|
|
46
|
+
position: 'left' | 'center' | 'right';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The message footer returned by the registry, composed of 'left', 'center',
|
|
51
|
+
* and 'right' sections.
|
|
52
|
+
*/
|
|
53
|
+
export type MessageFooter = {
|
|
54
|
+
left?: MessageFooterSection;
|
|
55
|
+
center?: MessageFooterSection;
|
|
56
|
+
right?: MessageFooterSection;
|
|
57
|
+
};
|
|
58
|
+
|
|
25
59
|
/**
|
|
26
60
|
* The default implementation of the message footer registry.
|
|
27
61
|
*/
|
|
@@ -43,10 +77,3 @@ export class MessageFooterRegistry implements IMessageFooterRegistry {
|
|
|
43
77
|
|
|
44
78
|
private _footers: MessageFooter = {};
|
|
45
79
|
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The token providing the chat footer registry.
|
|
49
|
-
*/
|
|
50
|
-
export const IMessageFooterRegistry = new Token<IMessageFooterRegistry>(
|
|
51
|
-
'@jupyter/chat:ChatFooterRegistry'
|
|
52
|
-
);
|
package/src/selection-watcher.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
7
7
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
8
8
|
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
9
|
+
import {
|
|
10
|
+
EditorLanguageRegistry,
|
|
11
|
+
IEditorLanguageRegistry
|
|
12
|
+
} from '@jupyterlab/codemirror';
|
|
9
13
|
import { Notebook } from '@jupyterlab/notebook';
|
|
10
14
|
|
|
11
15
|
import { find } from '@lumino/algorithm';
|
|
@@ -26,6 +30,10 @@ export namespace SelectionWatcher {
|
|
|
26
30
|
* The current shell of the application.
|
|
27
31
|
*/
|
|
28
32
|
shell: JupyterFrontEnd.IShell;
|
|
33
|
+
/**
|
|
34
|
+
* Editor language registry.
|
|
35
|
+
*/
|
|
36
|
+
languages?: IEditorLanguageRegistry;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
@@ -44,6 +52,10 @@ export namespace SelectionWatcher {
|
|
|
44
52
|
* The ID of the document widget in which the selection was made.
|
|
45
53
|
*/
|
|
46
54
|
widgetId: string;
|
|
55
|
+
/**
|
|
56
|
+
* The language of the selection.
|
|
57
|
+
*/
|
|
58
|
+
language?: string;
|
|
47
59
|
/**
|
|
48
60
|
* The ID of the cell in which the selection was made, if the original widget
|
|
49
61
|
* was a notebook.
|
|
@@ -70,6 +82,7 @@ export interface ISelectionWatcher {
|
|
|
70
82
|
export class SelectionWatcher {
|
|
71
83
|
constructor(options: SelectionWatcher.IOptions) {
|
|
72
84
|
this._shell = options.shell;
|
|
85
|
+
this._languages = options.languages || new EditorLanguageRegistry();
|
|
73
86
|
this._shell.currentChanged?.connect((sender, args) => {
|
|
74
87
|
// Do not change the main area widget if the new one has no editor, for example
|
|
75
88
|
// a chat panel. However, the selected text is only available if the main area
|
|
@@ -149,12 +162,15 @@ export class SelectionWatcher {
|
|
|
149
162
|
editor.setSelection({ start: newPosition, end: newPosition });
|
|
150
163
|
}
|
|
151
164
|
|
|
152
|
-
protected _poll(): void {
|
|
165
|
+
protected async _poll(): Promise<void> {
|
|
153
166
|
let currSelection: SelectionWatcher.Selection | null = null;
|
|
154
167
|
const prevSelection = this._selection;
|
|
155
168
|
// Do not return selected text if the main area widget is hidden.
|
|
156
169
|
if (this._mainAreaDocumentWidget?.isVisible) {
|
|
157
|
-
currSelection = getTextSelection(
|
|
170
|
+
currSelection = await getTextSelection(
|
|
171
|
+
this._mainAreaDocumentWidget,
|
|
172
|
+
this._languages
|
|
173
|
+
);
|
|
158
174
|
}
|
|
159
175
|
if (prevSelection?.text !== currSelection?.text) {
|
|
160
176
|
this._selection = currSelection;
|
|
@@ -169,14 +185,16 @@ export class SelectionWatcher {
|
|
|
169
185
|
this,
|
|
170
186
|
SelectionWatcher.Selection | null
|
|
171
187
|
>(this);
|
|
188
|
+
private _languages: IEditorLanguageRegistry;
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
/**
|
|
175
192
|
* Gets a Selection object from a document widget. Returns `null` if unable.
|
|
176
193
|
*/
|
|
177
|
-
function getTextSelection(
|
|
178
|
-
widget: Widget | null
|
|
179
|
-
|
|
194
|
+
async function getTextSelection(
|
|
195
|
+
widget: Widget | null,
|
|
196
|
+
languages: IEditorLanguageRegistry
|
|
197
|
+
): Promise<SelectionWatcher.Selection | null> {
|
|
180
198
|
const editor = getEditor(widget);
|
|
181
199
|
// widget type check is redundant but hints the type to TypeScript
|
|
182
200
|
if (!editor || !(widget instanceof DocumentWidget)) {
|
|
@@ -207,6 +225,7 @@ function getTextSelection(
|
|
|
207
225
|
[start, end] = [end, start];
|
|
208
226
|
}
|
|
209
227
|
|
|
228
|
+
const language = (await languages.getLanguage(editor?.model.mimeType))?.name;
|
|
210
229
|
return {
|
|
211
230
|
...selectionObj,
|
|
212
231
|
start,
|
|
@@ -214,6 +233,10 @@ function getTextSelection(
|
|
|
214
233
|
text,
|
|
215
234
|
numLines: text.split('\n').length,
|
|
216
235
|
widgetId: widget.id,
|
|
236
|
+
|
|
237
|
+
...(language && {
|
|
238
|
+
language
|
|
239
|
+
}),
|
|
217
240
|
...(cellId && {
|
|
218
241
|
cellId
|
|
219
242
|
})
|
package/src/types.ts
CHANGED
|
@@ -14,9 +14,14 @@ export interface IUser {
|
|
|
14
14
|
color?: string;
|
|
15
15
|
avatar_url?: string;
|
|
16
16
|
/**
|
|
17
|
-
* The string to use to mention a user in the chat.
|
|
17
|
+
* The string to use to mention a user in the chat. This is computed via the
|
|
18
|
+
* following procedure:
|
|
19
|
+
*
|
|
20
|
+
* 1. Let `mention_name = user.display_name || user.name || user.username`.
|
|
21
|
+
*
|
|
22
|
+
* 2. Replace each ' ' character with '-' in `mention_name`.
|
|
18
23
|
*/
|
|
19
|
-
mention_name
|
|
24
|
+
mention_name: string;
|
|
20
25
|
/**
|
|
21
26
|
* Boolean identifying if user is a bot.
|
|
22
27
|
*/
|