Skip to content

Commit de5eae9

Browse files
committed
feat: Add Enter key support for sending messages in chat
- Implement Enter key handler in TextEditor component to submit messages - Allow Shift+Enter for creating new lines in messages - Clear editor content automatically after message submission - Pass onSubmit callback from HelpPanel to TextEditor - Dynamically build ProseMirror plugins to support keyboard shortcuts This improves the chat UX by enabling quick message submission without needing to click the send button, matching standard chat application behavior.
1 parent 53cf695 commit de5eae9

2 files changed

Lines changed: 96 additions & 33 deletions

File tree

src/components/HelpPanel.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@ export const HelpPanel = ({
14081408
state={state}
14091409
disabled={isLoading}
14101410
placeholder={help.length > 0 ? "Reply to make changes..." : "What do you want to make today?"}
1411+
onSubmit={sendCombinedMessage}
14111412
/>
14121413
</div>
14131414
</div>
@@ -2204,7 +2205,7 @@ export const HelpPanel = ({
22042205
});
22052206
}
22062207
}}
2207-
title={message.taskId && onLoadTaskFromHelp ? `Click to ${isCollapsed ? 'expand and load' : 'collapse'} task ${message.taskId.slice(0, 8)}` : 'Click to expand/collapse'}
2208+
title={message.taskId && onLoadTaskFromHelp ? `Click to ${isCollapsed ? 'expand and load' : 'collapse'} task` : 'Click to expand/collapse'}
22082209
>
22092210
<div className="flex items-center space-x-2 flex-1 min-w-0">
22102211
<svg
@@ -2236,16 +2237,6 @@ export const HelpPanel = ({
22362237
)}
22372238
</div>
22382239
</div>
2239-
{isPending && (
2240-
<div className="flex items-center text-xs text-blue-600">
2241-
<span>Processing</span>
2242-
<div className="flex items-center ml-1">
2243-
<div className="w-1 h-1 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0ms', animationDuration: '1s' }}></div>
2244-
<div className="w-1 h-1 bg-blue-600 rounded-full mx-0.5 animate-bounce" style={{ animationDelay: '200ms', animationDuration: '1s' }}></div>
2245-
<div className="w-1 h-1 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '400ms', animationDuration: '1s' }}></div>
2246-
</div>
2247-
</div>
2248-
)}
22492240
</div>
22502241

22512242
{/* Collapsible content */}

src/components/TextEditor.tsx

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const createPlaceholderPlugin = (placeholder) => {
108108
});
109109
};
110110

111-
export const TextEditor = ({ state, placeholder = "", disabled = false }) => {
111+
export const TextEditor = ({ state, placeholder = "", disabled = false, onSubmit = null }) => {
112112
const [ editorView, setEditorView ] = useState(null);
113113
const [ hasFocus, setHasFocus ] = useState(false);
114114
const [ hasContent, setHasContent ] = useState(false);
@@ -153,32 +153,62 @@ export const TextEditor = ({ state, placeholder = "", disabled = false }) => {
153153
return false;
154154
};
155155

156-
const plugins = [
157-
history(),
158-
keymap({"Mod-z": undo, "Mod-y": redo, "Mod-`": toggleCodeBlock}),
159-
keymap(baseKeymap),
160-
new Plugin({
161-
props: {
162-
handleTextInput: handleBacktickInput
163-
}
164-
})
165-
];
166-
167-
// Add placeholder plugin if placeholder is provided
168-
if (placeholder) {
169-
plugins.push(createPlaceholderPlugin(placeholder));
170-
}
171-
172156
useEffect(() => {
173157
if (!editorRef.current) {
174158
return;
175159
}
160+
161+
// Handle Enter key to submit (Shift+Enter for new line)
162+
const handleEnterKey = (state, dispatch, view) => {
163+
// Only submit on plain Enter, not Shift+Enter
164+
if (onSubmit && !disabled) {
165+
const content = getEditorContent({ state });
166+
if (content && content.trim()) {
167+
onSubmit(content);
168+
// Clear the editor after submission
169+
if (dispatch) {
170+
const tr = state.tr.replaceWith(0, state.doc.content.size, schema.nodes.paragraph.create());
171+
dispatch(tr);
172+
}
173+
return true;
174+
}
175+
}
176+
return false;
177+
};
178+
179+
const customKeymap = {
180+
"Mod-z": undo,
181+
"Mod-y": redo,
182+
"Mod-`": toggleCodeBlock
183+
};
184+
185+
// Only add Enter handler if onSubmit is provided
186+
if (onSubmit) {
187+
customKeymap["Enter"] = handleEnterKey;
188+
// Allow Shift+Enter for new lines
189+
customKeymap["Shift-Enter"] = () => false; // Let default behavior handle it
190+
}
191+
192+
// Build plugins dynamically based on props
193+
const dynamicPlugins = [
194+
history(),
195+
keymap(customKeymap),
196+
keymap(baseKeymap),
197+
new Plugin({
198+
props: {
199+
handleTextInput: handleBacktickInput
200+
}
201+
})
202+
];
203+
204+
if (placeholder) {
205+
dynamicPlugins.push(createPlaceholderPlugin(placeholder));
206+
}
207+
176208
const editorView = new EditorView(editorRef.current, {
177209
state: EditorState.create({
178210
schema,
179-
plugins: [
180-
...plugins,
181-
],
211+
plugins: dynamicPlugins,
182212
}),
183213
dispatchTransaction(transaction) {
184214
if (disabled) return;
@@ -216,7 +246,7 @@ export const TextEditor = ({ state, placeholder = "", disabled = false }) => {
216246
editorView.destroy();
217247
}
218248
};
219-
}, [disabled]);
249+
}, [disabled, onSubmit, placeholder]);
220250

221251
// Update editor when disabled prop changes
222252
useEffect(() => {
@@ -233,6 +263,48 @@ export const TextEditor = ({ state, placeholder = "", disabled = false }) => {
233263
const { editorState } = state.data;
234264
useEffect(() => {
235265
if (editorState && editorView) {
266+
// Rebuild plugins for state update
267+
const handleEnterKey = (state, dispatch, view) => {
268+
if (onSubmit && !disabled) {
269+
const content = getEditorContent({ state });
270+
if (content && content.trim()) {
271+
onSubmit(content);
272+
if (dispatch) {
273+
const tr = state.tr.replaceWith(0, state.doc.content.size, schema.nodes.paragraph.create());
274+
dispatch(tr);
275+
}
276+
return true;
277+
}
278+
}
279+
return false;
280+
};
281+
282+
const customKeymap = {
283+
"Mod-z": undo,
284+
"Mod-y": redo,
285+
"Mod-`": toggleCodeBlock
286+
};
287+
288+
if (onSubmit) {
289+
customKeymap["Enter"] = handleEnterKey;
290+
customKeymap["Shift-Enter"] = () => false;
291+
}
292+
293+
const plugins = [
294+
history(),
295+
keymap(customKeymap),
296+
keymap(baseKeymap),
297+
new Plugin({
298+
props: {
299+
handleTextInput: handleBacktickInput
300+
}
301+
})
302+
];
303+
304+
if (placeholder) {
305+
plugins.push(createPlaceholderPlugin(placeholder));
306+
}
307+
236308
const newEditorState = EditorState.fromJSON({
237309
schema,
238310
plugins,
@@ -241,7 +313,7 @@ export const TextEditor = ({ state, placeholder = "", disabled = false }) => {
241313
// Update hasContent when editor state changes from props
242314
setHasContent(newEditorState.doc.textContent.length > 0);
243315
}
244-
}, [editorState]);
316+
}, [editorState, editorView, onSubmit, disabled, placeholder]);
245317

246318
return (
247319
<div className="relative">

0 commit comments

Comments
 (0)