mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
refactor & fix frontend: typing chat (#861)
This commit is contained in:
@@ -8,9 +8,8 @@ import { useTypingEffect } from "../hooks/useTypingEffect";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import {
|
||||
addAssistantMessageToChat,
|
||||
setCurrentQueueMarkerState,
|
||||
setCurrentTypingMsgState,
|
||||
setTypingActive,
|
||||
takeOneAndType,
|
||||
} from "../services/chatService";
|
||||
import { Message } from "../state/chatSlice";
|
||||
import { RootState } from "../store";
|
||||
@@ -29,25 +28,21 @@ interface IChatBubbleProps {
|
||||
*
|
||||
*/
|
||||
function TypingChat() {
|
||||
const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
|
||||
useSelector((state: RootState) => state.chat);
|
||||
const { typeThis } = useSelector((state: RootState) => state.chat);
|
||||
|
||||
const messageContent = useTypingEffect([currentTypingMessage], {
|
||||
const messageContent = useTypingEffect([typeThis?.content], {
|
||||
loop: false,
|
||||
setTypingActive,
|
||||
setCurrentQueueMarkerState,
|
||||
currentQueueMarker,
|
||||
playbackRate: 0.1,
|
||||
playbackRate: 0.099,
|
||||
addAssistantMessageToChat,
|
||||
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
|
||||
takeOneAndType,
|
||||
typeThis,
|
||||
});
|
||||
|
||||
return (
|
||||
currentQueueMarker !== null && (
|
||||
<Card className="bg-success-100">
|
||||
<CardBody>{messageContent}</CardBody>
|
||||
</Card>
|
||||
)
|
||||
<Card className="bg-success-100">
|
||||
<CardBody>{messageContent}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,14 +67,9 @@ function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
|
||||
|
||||
function MessageList(): JSX.Element {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
messages,
|
||||
queuedTyping,
|
||||
typingActive,
|
||||
currentQueueMarker,
|
||||
currentTypingMessage,
|
||||
newChatSequence,
|
||||
} = useSelector((state: RootState) => state.chat);
|
||||
const { typingActive, newChatSequence, typeThis } = useSelector(
|
||||
(state: RootState) => state.chat,
|
||||
);
|
||||
|
||||
const messageScroll = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
@@ -101,56 +91,17 @@ function MessageList(): JSX.Element {
|
||||
}, [newChatSequence, typingActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
|
||||
|
||||
if (
|
||||
currentQueueMarker !== null &&
|
||||
currentQueueMarker !== 0 &&
|
||||
currentTypingMessage !== newMessage
|
||||
) {
|
||||
setCurrentTypingMsgState(
|
||||
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
|
||||
);
|
||||
}
|
||||
}, [queuedTyping]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTypingMessage === "") return;
|
||||
if (typeThis.content === "") return;
|
||||
|
||||
if (!typingActive) setTypingActive(true);
|
||||
}, [currentTypingMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
|
||||
if (
|
||||
newMessage &&
|
||||
typingActive === false &&
|
||||
currentTypingMessage !== newMessage
|
||||
) {
|
||||
if (currentQueueMarker !== 0) {
|
||||
setCurrentTypingMsgState(
|
||||
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [typingActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentQueueMarker === 0) {
|
||||
setCurrentTypingMsgState(messages?.[queuedTyping?.[0]]?.content);
|
||||
}
|
||||
}, [currentQueueMarker]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typeThis]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{newChatSequence.map((msg, index) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
msg.sender === "user" || msg.sender === "assistant" ? (
|
||||
<ChatBubble key={index} msg={msg} />
|
||||
) : (
|
||||
<div key={index} />
|
||||
),
|
||||
)}
|
||||
{newChatSequence.map((msg, index) => (
|
||||
<ChatBubble key={index} msg={msg} />
|
||||
))}
|
||||
|
||||
{typingActive && (
|
||||
<div className="flex mb-2.5 pr-5 pl-5 bg-s">
|
||||
|
||||
@@ -9,25 +9,23 @@ export const useTypingEffect = (
|
||||
loop = false,
|
||||
playbackRate = 0.1,
|
||||
setTypingActive = () => {},
|
||||
setCurrentQueueMarkerState = () => {},
|
||||
currentQueueMarker = 0,
|
||||
addAssistantMessageToChat = () => {},
|
||||
assistantMessageObj = { content: "", sender: "assistant" },
|
||||
takeOneAndType = () => {},
|
||||
typeThis = { content: "", sender: "assistant" },
|
||||
}: {
|
||||
loop?: boolean;
|
||||
playbackRate?: number;
|
||||
setTypingActive?: (bool: boolean) => void;
|
||||
setCurrentQueueMarkerState?: (marker: number) => void;
|
||||
currentQueueMarker?: number;
|
||||
addAssistantMessageToChat?: (msg: Message) => void;
|
||||
assistantMessageObj?: Message;
|
||||
takeOneAndType?: () => void;
|
||||
typeThis?: Message;
|
||||
} = {
|
||||
loop: false,
|
||||
playbackRate: 0.1,
|
||||
setTypingActive: () => {},
|
||||
currentQueueMarker: 0,
|
||||
addAssistantMessageToChat: () => {},
|
||||
assistantMessageObj: { content: "", sender: "assistant" },
|
||||
takeOneAndType: () => {},
|
||||
typeThis: { content: "", sender: "assistant" },
|
||||
},
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -50,8 +48,8 @@ export const useTypingEffect = (
|
||||
if (stringIndex === strings.length) {
|
||||
if (!loop) {
|
||||
setTypingActive(false);
|
||||
setCurrentQueueMarkerState(currentQueueMarker + 1);
|
||||
addAssistantMessageToChat(assistantMessageObj);
|
||||
addAssistantMessageToChat(typeThis);
|
||||
takeOneAndType();
|
||||
return;
|
||||
}
|
||||
stringIndex = 0;
|
||||
@@ -73,6 +71,7 @@ export const useTypingEffect = (
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const nonBreakingSpace = "\u00A0";
|
||||
|
||||
@@ -2,9 +2,7 @@ import {
|
||||
Message,
|
||||
appendToNewChatSequence,
|
||||
appendUserMessage,
|
||||
emptyOutQueuedTyping,
|
||||
setCurrentQueueMarker,
|
||||
setCurrentTypingMessage,
|
||||
takeOneTypeIt,
|
||||
toggleTypingActive,
|
||||
} from "../state/chatSlice";
|
||||
import Socket from "./socket";
|
||||
@@ -39,17 +37,9 @@ export function sendChatMessageFromEvent(event: string | SocketMessage): void {
|
||||
export function setTypingActive(bool: boolean): void {
|
||||
store.dispatch(toggleTypingActive(bool));
|
||||
}
|
||||
|
||||
export function resetQueuedTyping(): void {
|
||||
store.dispatch(emptyOutQueuedTyping());
|
||||
}
|
||||
|
||||
export function setCurrentTypingMsgState(msg: string): void {
|
||||
store.dispatch(setCurrentTypingMessage(msg));
|
||||
}
|
||||
export function setCurrentQueueMarkerState(index: number): void {
|
||||
store.dispatch(setCurrentQueueMarker(index));
|
||||
}
|
||||
export function addAssistantMessageToChat(msg: Message): void {
|
||||
store.dispatch(appendToNewChatSequence(msg));
|
||||
}
|
||||
export function takeOneAndType(): void {
|
||||
store.dispatch(takeOneTypeIt());
|
||||
}
|
||||
|
||||
@@ -6,19 +6,16 @@ export type Message = {
|
||||
};
|
||||
|
||||
const initialMessages: Message[] = [];
|
||||
const queuedMessages: number[] = [];
|
||||
const currentQueueMarker: number = 0;
|
||||
export const chatSlice = createSlice({
|
||||
name: "chat",
|
||||
initialState: {
|
||||
messages: initialMessages,
|
||||
queuedTyping: queuedMessages,
|
||||
typingActive: false,
|
||||
currentTypingMessage: "",
|
||||
currentQueueMarker,
|
||||
userMessages: initialMessages,
|
||||
assistantMessages: initialMessages,
|
||||
assistantMessagesTypingQueue: initialMessages,
|
||||
newChatSequence: initialMessages,
|
||||
typeThis: { content: "", sender: "assistant" } as Message,
|
||||
},
|
||||
reducers: {
|
||||
appendUserMessage: (state, action) => {
|
||||
@@ -28,30 +25,40 @@ export const chatSlice = createSlice({
|
||||
},
|
||||
appendAssistantMessage: (state, action) => {
|
||||
state.messages.push({ content: action.payload, sender: "assistant" });
|
||||
state.assistantMessages.push({
|
||||
content: action.payload,
|
||||
sender: "assistant",
|
||||
});
|
||||
// state.queuedTyping.push(action.payload);
|
||||
const assistantMessageIndex = state.messages.length - 1;
|
||||
state.queuedTyping.push(assistantMessageIndex);
|
||||
},
|
||||
setCurrentQueueMarker: (state, action) => {
|
||||
state.currentQueueMarker = action.payload;
|
||||
|
||||
if (
|
||||
state.assistantMessagesTypingQueue.length > 0 ||
|
||||
state.typingActive === true
|
||||
) {
|
||||
state.assistantMessagesTypingQueue.push({
|
||||
content: action.payload,
|
||||
sender: "assistant",
|
||||
});
|
||||
} else if (
|
||||
state.assistantMessagesTypingQueue.length === 0 &&
|
||||
state.typingActive === false
|
||||
) {
|
||||
state.typeThis = {
|
||||
content: action.payload,
|
||||
sender: "assistant",
|
||||
};
|
||||
state.typingActive = true;
|
||||
}
|
||||
},
|
||||
|
||||
toggleTypingActive: (state, action) => {
|
||||
state.typingActive = action.payload;
|
||||
},
|
||||
emptyOutQueuedTyping: (state) => {
|
||||
state.queuedTyping = [];
|
||||
},
|
||||
setCurrentTypingMessage: (state, action) => {
|
||||
state.currentTypingMessage = action.payload;
|
||||
// state.currentQueueMarker += 1;
|
||||
},
|
||||
|
||||
appendToNewChatSequence: (state, action) => {
|
||||
state.newChatSequence.push(action.payload);
|
||||
},
|
||||
|
||||
takeOneTypeIt: (state) => {
|
||||
if (state.assistantMessagesTypingQueue.length > 0) {
|
||||
state.typeThis = state.assistantMessagesTypingQueue.shift() as Message;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,10 +66,8 @@ export const {
|
||||
appendUserMessage,
|
||||
appendAssistantMessage,
|
||||
toggleTypingActive,
|
||||
emptyOutQueuedTyping,
|
||||
setCurrentTypingMessage,
|
||||
setCurrentQueueMarker,
|
||||
appendToNewChatSequence,
|
||||
takeOneTypeIt,
|
||||
} = chatSlice.actions;
|
||||
|
||||
export default chatSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user