@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.
- package/lib/__tests__/chat-input.spec.d.ts +1 -0
- package/lib/__tests__/chat-input.spec.js +72 -0
- package/lib/__tests__/model.spec.js +12 -0
- package/lib/components/input/chat-input.js +6 -1
- package/lib/components/messages/message.js +2 -1
- package/lib/components/messages/welcome.js +1 -1
- package/lib/input-model.d.ts +6 -0
- package/lib/model.js +2 -1
- package/lib/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/__tests__/chat-input.spec.tsx +100 -0
- package/src/__tests__/model.spec.ts +15 -0
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/messages/message.tsx +2 -1
- package/src/components/messages/welcome.tsx +1 -1
- package/src/input-model.ts +6 -0
- package/src/model.ts +2 -1
- package/src/types.ts +6 -0
- package/style/chat.css +1 -0
|
@@ -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:
|
|
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
|
|
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
|
/**
|
package/lib/input-model.d.ts
CHANGED
|
@@ -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
|
@@ -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={
|
|
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
|
|
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);
|
package/src/input-model.ts
CHANGED
|
@@ -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
|
/**
|