@jupyter/chat 0.25.0 → 0.25.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import React, { act } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+ // React 18 asks test environments to declare themselves, otherwise every
8
+ // `act` call warns.
9
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
10
+ import { ChatInput } from '../components/input/chat-input';
11
+ import { ChatReactContext } from '../context';
12
+ import { MockChatModel } from './mocks';
13
+ const DEFAULT_PLACEHOLDER = 'Type a chat message, @ to mention...';
14
+ describe('ChatInput placeholder', () => {
15
+ let container;
16
+ let root;
17
+ const render = (config) => {
18
+ const model = new MockChatModel({ config });
19
+ act(() => {
20
+ root.render(React.createElement(ChatReactContext.Provider, { value: { model, rmRegistry: {} } },
21
+ React.createElement(ChatInput, { model: model.input })));
22
+ });
23
+ return model;
24
+ };
25
+ const placeholder = () => container.querySelector('[role="combobox"]').getAttribute('placeholder');
26
+ beforeEach(() => {
27
+ container = document.createElement('div');
28
+ document.body.appendChild(container);
29
+ root = createRoot(container);
30
+ });
31
+ afterEach(() => {
32
+ act(() => {
33
+ root.unmount();
34
+ });
35
+ container.remove();
36
+ });
37
+ it('should use the default placeholder when none is configured', () => {
38
+ render();
39
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
40
+ });
41
+ it('should use the configured placeholder', () => {
42
+ render({ inputPlaceholder: 'Ask the assistant' });
43
+ expect(placeholder()).toBe('Ask the assistant');
44
+ });
45
+ it('should use an empty configured placeholder as is', () => {
46
+ render({ inputPlaceholder: '' });
47
+ expect(placeholder()).toBe('');
48
+ });
49
+ it('should follow a later change of the configuration', () => {
50
+ const model = render();
51
+ act(() => {
52
+ model.config = { inputPlaceholder: 'Ask the assistant' };
53
+ });
54
+ expect(placeholder()).toBe('Ask the assistant');
55
+ // Unsetting it is the way back to the default, an empty string is not.
56
+ act(() => {
57
+ model.config = { inputPlaceholder: '' };
58
+ });
59
+ expect(placeholder()).toBe('');
60
+ act(() => {
61
+ model.config = { inputPlaceholder: undefined };
62
+ });
63
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
64
+ });
65
+ it('should keep the default placeholder on an unrelated change', () => {
66
+ const model = render();
67
+ act(() => {
68
+ model.config = { sendWithShiftEnter: true };
69
+ });
70
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
71
+ });
72
+ });
@@ -122,6 +122,18 @@ describe('test chat model', () => {
122
122
  const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
123
123
  expect(model.config.sendWithShiftEnter).toBeTruthy();
124
124
  });
125
+ it('should forward the input placeholder to the input model', () => {
126
+ const model = new MockChatModel({
127
+ config: { inputPlaceholder: 'Ask the assistant' }
128
+ });
129
+ expect(model.input.config.inputPlaceholder).toBe('Ask the assistant');
130
+ });
131
+ it('should forward a later input placeholder change to the input model', () => {
132
+ const model = new MockChatModel();
133
+ expect(model.input.config.inputPlaceholder).toBeUndefined();
134
+ model.config = { inputPlaceholder: 'Ask the assistant' };
135
+ expect(model.input.config.inputPlaceholder).toBe('Ask the assistant');
136
+ });
125
137
  });
126
138
  describe('awareness', () => {
127
139
  it('should surface the model awareness on the context', () => {
@@ -22,6 +22,7 @@ export function ChatInput(props) {
22
22
  const inputRef = useRef();
23
23
  const chatCommands = useChatCommands(model, chatCommandRegistry);
24
24
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState((_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
25
+ const [placeholder, setPlaceholder] = useState(model.config.inputPlaceholder);
25
26
  const [attachments, setAttachments] = useState(model.attachments);
26
27
  const [toolbarElements, setToolbarElements] = useState([]);
27
28
  /**
@@ -47,6 +48,7 @@ export function ChatInput(props) {
47
48
  const configChanged = (_, config) => {
48
49
  var _a;
49
50
  setSendWithShiftEnter((_a = config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false);
51
+ setPlaceholder(config.inputPlaceholder);
50
52
  };
51
53
  model.configChanged.connect(configChanged);
52
54
  const focusInputElement = () => {
@@ -80,6 +82,9 @@ export function ChatInput(props) {
80
82
  };
81
83
  }, [inputToolbarRegistry]);
82
84
  const inputExists = !!input.trim();
85
+ // The default is resolved here rather than in `IConfig`, so that it can be
86
+ // translated. Only `undefined` falls back; an empty string is a valid choice.
87
+ const inputPlaceholder = placeholder !== null && placeholder !== void 0 ? placeholder : trans.__('Type a chat message, @ to mention...');
83
88
  /**
84
89
  * `handleKeyDown()`: callback invoked when the user presses any key in the
85
90
  * `TextField` component. This is used to send the message when a user presses
@@ -163,7 +168,7 @@ export function ChatInput(props) {
163
168
  padding: 0
164
169
  }
165
170
  }
166
- }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, onKeyDown: handleKeyDown, placeholder: trans.__('Type a chat message, @ to mention...'), inputRef: inputRef, onSelect: () => { var _a, _b; return (model.cursorIndex = (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : null); }, sx: {
171
+ }, renderInput: params => (React.createElement(TextField, { ...params, fullWidth: true, variant: "standard", className: INPUT_TEXTFIELD_CLASS, multiline: true, maxRows: 10, onKeyDown: handleKeyDown, placeholder: inputPlaceholder, inputRef: inputRef, onSelect: () => { var _a, _b; return (model.cursorIndex = (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : null); }, sx: {
167
172
  padding: 1.5,
168
173
  margin: 0,
169
174
  boxSizing: 'border-box',
@@ -77,7 +77,8 @@ export function ChatMessageBase(props) {
77
77
  selectionWatcher: model.selectionWatcher,
78
78
  documentManager: model.documentManager,
79
79
  config: {
80
- sendWithShiftEnter: model.config.sendWithShiftEnter
80
+ sendWithShiftEnter: model.config.sendWithShiftEnter,
81
+ inputPlaceholder: model.config.inputPlaceholder
81
82
  },
82
83
  attachments: structuredClone((_b = message.attachments) !== null && _b !== void 0 ? _b : []),
83
84
  mentions: message.mentions
@@ -15,7 +15,7 @@ const MD_MIME_TYPE = 'text/markdown';
15
15
  */
16
16
  export function WelcomeMessage(props) {
17
17
  const { rmRegistry } = useChatContext();
18
- const content = props.content + '\n----\n';
18
+ const content = props.content + '\n\n---\n';
19
19
  // ref that tracks the content container to store the rendermime node in
20
20
  const renderingContainer = useRef(null);
21
21
  /**
@@ -385,5 +385,11 @@ export declare namespace InputModel {
385
385
  * Whether to display the 'send with selection' button.
386
386
  */
387
387
  sendWithSelection?: boolean;
388
+ /**
389
+ * The placeholder to display in the empty chat input.
390
+ * The default placeholder is used if undefined, an empty string displays no
391
+ * placeholder at all.
392
+ */
393
+ inputPlaceholder?: string;
388
394
  }
389
395
  }
package/lib/model.js CHANGED
@@ -54,7 +54,8 @@ export class AbstractChatModel {
54
54
  selectionWatcher: options.selectionWatcher,
55
55
  documentManager: options.documentManager,
56
56
  config: {
57
- sendWithShiftEnter: config.sendWithShiftEnter
57
+ sendWithShiftEnter: config.sendWithShiftEnter,
58
+ inputPlaceholder: config.inputPlaceholder
58
59
  },
59
60
  onSend: this.sendMessage.bind(this)
60
61
  });
package/lib/types.d.ts CHANGED
@@ -57,6 +57,12 @@ export interface IConfig {
57
57
  * Whether to display the 'send with selection' button.
58
58
  */
59
59
  sendWithSelection?: boolean;
60
+ /**
61
+ * The placeholder to display in the empty chat input.
62
+ * The default placeholder is used if undefined, an empty string displays no
63
+ * placeholder at all.
64
+ */
65
+ inputPlaceholder?: string;
60
66
  }
61
67
  /**
62
68
  * Mime model body type, a partial mime bundle model containing at least the data.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -0,0 +1,100 @@
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, { act } from 'react';
8
+ import { createRoot, Root } from 'react-dom/client';
9
+
10
+ // React 18 asks test environments to declare themselves, otherwise every
11
+ // `act` call warns.
12
+ (
13
+ globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }
14
+ ).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ import { ChatInput } from '../components/input/chat-input';
17
+ import { ChatReactContext } from '../context';
18
+ import { IConfig } from '../types';
19
+ import { MockChatModel } from './mocks';
20
+
21
+ const DEFAULT_PLACEHOLDER = 'Type a chat message, @ to mention...';
22
+
23
+ describe('ChatInput placeholder', () => {
24
+ let container: HTMLDivElement;
25
+ let root: Root;
26
+
27
+ const render = (config?: IConfig): MockChatModel => {
28
+ const model = new MockChatModel({ config });
29
+ act(() => {
30
+ root.render(
31
+ <ChatReactContext.Provider
32
+ value={{ model, rmRegistry: {} as IRenderMimeRegistry }}
33
+ >
34
+ <ChatInput model={model.input} />
35
+ </ChatReactContext.Provider>
36
+ );
37
+ });
38
+ return model;
39
+ };
40
+
41
+ const placeholder = (): string | null =>
42
+ container.querySelector('[role="combobox"]')!.getAttribute('placeholder');
43
+
44
+ beforeEach(() => {
45
+ container = document.createElement('div');
46
+ document.body.appendChild(container);
47
+ root = createRoot(container);
48
+ });
49
+
50
+ afterEach(() => {
51
+ act(() => {
52
+ root.unmount();
53
+ });
54
+ container.remove();
55
+ });
56
+
57
+ it('should use the default placeholder when none is configured', () => {
58
+ render();
59
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
60
+ });
61
+
62
+ it('should use the configured placeholder', () => {
63
+ render({ inputPlaceholder: 'Ask the assistant' });
64
+ expect(placeholder()).toBe('Ask the assistant');
65
+ });
66
+
67
+ it('should use an empty configured placeholder as is', () => {
68
+ render({ inputPlaceholder: '' });
69
+ expect(placeholder()).toBe('');
70
+ });
71
+
72
+ it('should follow a later change of the configuration', () => {
73
+ const model = render();
74
+
75
+ act(() => {
76
+ model.config = { inputPlaceholder: 'Ask the assistant' };
77
+ });
78
+ expect(placeholder()).toBe('Ask the assistant');
79
+
80
+ // Unsetting it is the way back to the default, an empty string is not.
81
+ act(() => {
82
+ model.config = { inputPlaceholder: '' };
83
+ });
84
+ expect(placeholder()).toBe('');
85
+
86
+ act(() => {
87
+ model.config = { inputPlaceholder: undefined };
88
+ });
89
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
90
+ });
91
+
92
+ it('should keep the default placeholder on an unrelated change', () => {
93
+ const model = render();
94
+
95
+ act(() => {
96
+ model.config = { sendWithShiftEnter: true };
97
+ });
98
+ expect(placeholder()).toBe(DEFAULT_PLACEHOLDER);
99
+ });
100
+ });
@@ -149,6 +149,21 @@ describe('test chat model', () => {
149
149
  const model = new MockChatModel({ config: { sendWithShiftEnter: true } });
150
150
  expect(model.config.sendWithShiftEnter).toBeTruthy();
151
151
  });
152
+
153
+ it('should forward the input placeholder to the input model', () => {
154
+ const model = new MockChatModel({
155
+ config: { inputPlaceholder: 'Ask the assistant' }
156
+ });
157
+ expect(model.input.config.inputPlaceholder).toBe('Ask the assistant');
158
+ });
159
+
160
+ it('should forward a later input placeholder change to the input model', () => {
161
+ const model = new MockChatModel();
162
+ expect(model.input.config.inputPlaceholder).toBeUndefined();
163
+
164
+ model.config = { inputPlaceholder: 'Ask the assistant' };
165
+ expect(model.input.config.inputPlaceholder).toBe('Ask the assistant');
166
+ });
152
167
  });
153
168
 
154
169
  describe('awareness', () => {
@@ -40,6 +40,9 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
40
40
  const [sendWithShiftEnter, setSendWithShiftEnter] = useState<boolean>(
41
41
  model.config.sendWithShiftEnter ?? false
42
42
  );
43
+ const [placeholder, setPlaceholder] = useState<string | undefined>(
44
+ model.config.inputPlaceholder
45
+ );
43
46
  const [attachments, setAttachments] = useState<IAttachment[]>(
44
47
  model.attachments
45
48
  );
@@ -70,6 +73,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
70
73
 
71
74
  const configChanged = (_: IInputModel, config: InputModel.IConfig) => {
72
75
  setSendWithShiftEnter(config.sendWithShiftEnter ?? false);
76
+ setPlaceholder(config.inputPlaceholder);
73
77
  };
74
78
  model.configChanged.connect(configChanged);
75
79
 
@@ -110,6 +114,11 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
110
114
 
111
115
  const inputExists = !!input.trim();
112
116
 
117
+ // The default is resolved here rather than in `IConfig`, so that it can be
118
+ // translated. Only `undefined` falls back; an empty string is a valid choice.
119
+ const inputPlaceholder =
120
+ placeholder ?? trans.__('Type a chat message, @ to mention...');
121
+
113
122
  /**
114
123
  * `handleKeyDown()`: callback invoked when the user presses any key in the
115
124
  * `TextField` component. This is used to send the message when a user presses
@@ -231,7 +240,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
231
240
  multiline
232
241
  maxRows={10}
233
242
  onKeyDown={handleKeyDown}
234
- placeholder={trans.__('Type a chat message, @ to mention...')}
243
+ placeholder={inputPlaceholder}
235
244
  inputRef={inputRef}
236
245
  onSelect={() =>
237
246
  (model.cursorIndex = inputRef.current?.selectionStart ?? null)
@@ -104,7 +104,8 @@ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
104
104
  selectionWatcher: model.selectionWatcher,
105
105
  documentManager: model.documentManager,
106
106
  config: {
107
- sendWithShiftEnter: model.config.sendWithShiftEnter
107
+ sendWithShiftEnter: model.config.sendWithShiftEnter,
108
+ inputPlaceholder: model.config.inputPlaceholder
108
109
  },
109
110
  attachments: structuredClone(message.attachments ?? []),
110
111
  mentions: message.mentions
@@ -29,7 +29,7 @@ export interface IWelcomeMessageProps {
29
29
  */
30
30
  export function WelcomeMessage(props: IWelcomeMessageProps): JSX.Element {
31
31
  const { rmRegistry } = useChatContext();
32
- const content = props.content + '\n----\n';
32
+ const content = props.content + '\n\n---\n';
33
33
 
34
34
  // ref that tracks the content container to store the rendermime node in
35
35
  const renderingContainer = useRef<HTMLDivElement | null>(null);
@@ -685,6 +685,12 @@ export namespace InputModel {
685
685
  * Whether to display the 'send with selection' button.
686
686
  */
687
687
  sendWithSelection?: boolean;
688
+ /**
689
+ * The placeholder to display in the empty chat input.
690
+ * The default placeholder is used if undefined, an empty string displays no
691
+ * placeholder at all.
692
+ */
693
+ inputPlaceholder?: string;
688
694
  }
689
695
  }
690
696
 
package/src/model.ts CHANGED
@@ -301,7 +301,8 @@ export abstract class AbstractChatModel implements IChatModel {
301
301
  selectionWatcher: options.selectionWatcher,
302
302
  documentManager: options.documentManager,
303
303
  config: {
304
- sendWithShiftEnter: config.sendWithShiftEnter
304
+ sendWithShiftEnter: config.sendWithShiftEnter,
305
+ inputPlaceholder: config.inputPlaceholder
305
306
  },
306
307
  onSend: this.sendMessage.bind(this)
307
308
  });
package/src/types.ts CHANGED
@@ -64,6 +64,12 @@ export interface IConfig {
64
64
  * Whether to display the 'send with selection' button.
65
65
  */
66
66
  sendWithSelection?: boolean;
67
+ /**
68
+ * The placeholder to display in the empty chat input.
69
+ * The default placeholder is used if undefined, an empty string displays no
70
+ * placeholder at all.
71
+ */
72
+ inputPlaceholder?: string;
67
73
  }
68
74
 
69
75
  /**
package/style/chat.css CHANGED
@@ -32,6 +32,7 @@
32
32
  padding: 0 1em;
33
33
  }
34
34
 
35
+ .jp-ThemedContainer .jp-chat-welcome-message .jp-RenderedHTMLCommon,
35
36
  .jp-ThemedContainer .jp-chat-rendered-message .jp-RenderedHTMLCommon {
36
37
  padding-right: 0;
37
38
  }