mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix: block input send event while ime composition (#701)
* fix: trigger send event while ime composition & separate input element & disable input event while initializing * fix: eslint react plugin setting --------- Co-authored-by: Jim Su <jimsu@protonmail.com>
This commit is contained in:
parent
0748f0b7ce
commit
baa981cda7
@ -8,12 +8,19 @@
|
||||
"airbnb-typescript",
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": ["error"]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
@ -43,4 +50,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.1.6",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
@ -52,12 +53,6 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/js-with-ts",
|
||||
"testEnvironment": "jest-environment-jsdom",
|
||||
|
||||
3
frontend/pnpm-lock.yaml
generated
3
frontend/pnpm-lock.yaml
generated
@ -65,6 +65,9 @@ dependencies:
|
||||
react-syntax-highlighter:
|
||||
specifier: ^15.5.0
|
||||
version: 15.5.0(react@18.2.0)
|
||||
tailwind-merge:
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
typescript:
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { Card, CardBody, Textarea } from "@nextui-org/react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Card, CardBody } from "@nextui-org/react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import assistantAvatar from "../assets/assistant-avatar.png";
|
||||
import CogTooth from "../assets/cog-tooth";
|
||||
import userAvatar from "../assets/user-avatar.png";
|
||||
import { useTypingEffect } from "../hooks/useTypingEffect";
|
||||
import {
|
||||
sendChatMessage,
|
||||
setCurrentQueueMarkerState,
|
||||
setCurrentTypingMsgState,
|
||||
setTypingAcitve,
|
||||
addAssistanctMessageToChat,
|
||||
addAssistantMessageToChat,
|
||||
} from "../services/chatService";
|
||||
import { RootState } from "../store";
|
||||
import { Message } from "../state/chatSlice";
|
||||
import Input from "./Input";
|
||||
|
||||
interface IChatBubbleProps {
|
||||
msg: Message;
|
||||
@ -31,25 +31,22 @@ function TypingChat() {
|
||||
const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
|
||||
useSelector((state: RootState) => state.chat);
|
||||
|
||||
const messageContent = useTypingEffect([currentTypingMessage], {
|
||||
loop: false,
|
||||
setTypingAcitve,
|
||||
setCurrentQueueMarkerState,
|
||||
currentQueueMarker,
|
||||
playbackRate: 0.1,
|
||||
addAssistantMessageToChat,
|
||||
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
|
||||
});
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{currentQueueMarker !== null && (
|
||||
<Card className="bg-success-100">
|
||||
<CardBody>
|
||||
{useTypingEffect([currentTypingMessage], {
|
||||
loop: false,
|
||||
setTypingAcitve,
|
||||
setCurrentQueueMarkerState,
|
||||
currentQueueMarker,
|
||||
playbackRate: 0.1,
|
||||
addAssistanctMessageToChat,
|
||||
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
|
||||
})}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
currentQueueMarker !== null && (
|
||||
<Card className="bg-success-100">
|
||||
<CardBody>{messageContent}</CardBody>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -190,14 +187,6 @@ interface Props {
|
||||
|
||||
function ChatInterface({ setSettingOpen }: Props): JSX.Element {
|
||||
const { initialized } = useSelector((state: RootState) => state.task);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (inputMessage.trim() !== "") {
|
||||
sendChatMessage(inputMessage);
|
||||
setInputMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-0 bg-bg-light">
|
||||
@ -211,35 +200,7 @@ function ChatInterface({ setSettingOpen }: Props): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
{initialized ? <MessageList /> : <InitializingStatus />}
|
||||
<div className="w-full relative text-base">
|
||||
<Textarea
|
||||
className="py-4 px-4"
|
||||
classNames={{
|
||||
input: "pr-16 py-2",
|
||||
}}
|
||||
value={inputMessage}
|
||||
maxRows={10}
|
||||
minRows={1}
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
e.target.value !== "\n" && setInputMessage(e.target.value)
|
||||
}
|
||||
placeholder="Send a message (won't interrupt the Assistant)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-transparent border-none rounded py-2.5 px-5 hover:opacity-80 cursor-pointer select-none absolute right-5 bottom-6"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!initialized}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<Input />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
74
frontend/src/components/Input.tsx
Normal file
74
frontend/src/components/Input.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { ChangeEvent, useState, KeyboardEvent } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Textarea } from "@nextui-org/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { RootState } from "../store";
|
||||
import useInputComposition from "../hooks/useInputComposition";
|
||||
import { sendChatMessage } from "../services/chatService";
|
||||
|
||||
function Input() {
|
||||
const { initialized } = useSelector((state: RootState) => state.task);
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (inputMessage.trim() !== "") {
|
||||
sendChatMessage(inputMessage);
|
||||
setInputMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
const { onCompositionEnd, onCompositionStart, isComposing } =
|
||||
useInputComposition();
|
||||
|
||||
const handleChangeInputMessage = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value !== "\n") {
|
||||
setInputMessage(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessageOnEnter = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
// Prevent "Enter" from sending during IME input (e.g., Chinese, Japanese)
|
||||
if (isComposing) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative text-base">
|
||||
<Textarea
|
||||
disabled={!initialized}
|
||||
className="py-4 px-4"
|
||||
classNames={{
|
||||
input: "pr-16 py-2",
|
||||
}}
|
||||
value={inputMessage}
|
||||
maxRows={10}
|
||||
minRows={1}
|
||||
variant="bordered"
|
||||
onChange={handleChangeInputMessage}
|
||||
onKeyDown={handleSendMessageOnEnter}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
placeholder="Send a message (won't interrupt the Assistant)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"bg-transparent border-none rounded py-2.5 px-5 hover:opacity-80 cursor-pointer select-none absolute right-5 bottom-6",
|
||||
!initialized && "cursor-not-allowed opacity-80",
|
||||
)}
|
||||
onClick={handleSendMessage}
|
||||
disabled={!initialized}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Input;
|
||||
19
frontend/src/hooks/useInputComposition.ts
Normal file
19
frontend/src/hooks/useInputComposition.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const useInputComposition = () => {
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
const handleCompositionStart = () => {
|
||||
setIsComposing(true);
|
||||
};
|
||||
const handleCompositionEnd = () => {
|
||||
setIsComposing(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isComposing,
|
||||
onCompositionStart: handleCompositionStart,
|
||||
onCompositionEnd: handleCompositionEnd,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInputComposition;
|
||||
@ -11,7 +11,7 @@ export const useTypingEffect = (
|
||||
setTypingAcitve = () => {},
|
||||
setCurrentQueueMarkerState = () => {},
|
||||
currentQueueMarker = 0,
|
||||
addAssistanctMessageToChat = () => {},
|
||||
addAssistantMessageToChat = () => {},
|
||||
assistantMessageObj = { content: "", sender: "assistant" },
|
||||
}: {
|
||||
loop?: boolean;
|
||||
@ -19,14 +19,14 @@ export const useTypingEffect = (
|
||||
setTypingAcitve?: (bool: boolean) => void;
|
||||
setCurrentQueueMarkerState?: (marker: number) => void;
|
||||
currentQueueMarker?: number;
|
||||
addAssistanctMessageToChat?: (msg: Message) => void;
|
||||
addAssistantMessageToChat?: (msg: Message) => void;
|
||||
assistantMessageObj?: Message;
|
||||
} = {
|
||||
loop: false,
|
||||
playbackRate: 0.1,
|
||||
setTypingAcitve: () => {},
|
||||
currentQueueMarker: 0,
|
||||
addAssistanctMessageToChat: () => {},
|
||||
addAssistantMessageToChat: () => {},
|
||||
assistantMessageObj: { content: "", sender: "assistant" },
|
||||
},
|
||||
) => {
|
||||
@ -51,7 +51,7 @@ export const useTypingEffect = (
|
||||
if (!loop) {
|
||||
setTypingAcitve(false);
|
||||
setCurrentQueueMarkerState(currentQueueMarker + 1);
|
||||
addAssistanctMessageToChat(assistantMessageObj);
|
||||
addAssistantMessageToChat(assistantMessageObj);
|
||||
return;
|
||||
}
|
||||
stringIndex = 0;
|
||||
|
||||
@ -31,6 +31,6 @@ export function setCurrentTypingMsgState(msg: string): void {
|
||||
export function setCurrentQueueMarkerState(index: number): void {
|
||||
store.dispatch(setCurrentQueueMarker(index));
|
||||
}
|
||||
export function addAssistanctMessageToChat(msg: Message): void {
|
||||
export function addAssistantMessageToChat(msg: Message): void {
|
||||
store.dispatch(appeendToNewChatSequence(msg));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user