Show sent messages before the server confirms them

The composer now puts the bubble and clears the input as soon as you send, so a slow connection never holds either. The bot's status row reads "<name> is getting your message..." until the saved message arrives (by response or push), then switches to thinking. A failed send removes the bubble and restores the text and files.
This commit is contained in:
2026-10-20 14:50:20 +01:00
parent 4f7e0a6bf0
commit 8425631b3a
9 changed files with 323 additions and 42 deletions
@@ -16,6 +16,7 @@ import { CrewWorkspacesContext } from "../(data)/crew-workspaces-context";
import { BotMessageActionsContext } from "../(data)/message-actions-context";
import { SpeakMessageContext } from "../(data)/speak-message-context";
import reactToBotMessage from "../(functions)/react-to-bot-message";
import dropSentMessage from "../(utils)/drop-sent-message";
import useBotComputers from "../(hooks)/use-bot-computers";
import useTaskProgress from "../(hooks)/use-task-progress";
import useReplyProgress from "../(hooks)/use-reply-progress";
@@ -45,6 +46,15 @@ export default function Conversation() {
const [messages, setMessages] = useState<BotMessageWithAttachments[]>(
pageProps?.bot_messages || [],
);
/**
* Messages sent here that the server hasn't confirmed yet, shown after
* the saved ones so a slow connection never hides what was typed. They
* give way to the saved message, which arrives by the response or a
* push.
*/
const [pending_messages, setPendingMessages] = useState<
BotMessageWithAttachments[]
>([]);
const [session_id, setSessionId] = useState<number | null>(
pageProps?.bot_session?.id || null,
);
@@ -54,10 +64,12 @@ export default function Conversation() {
);
/**
* A quote picked in one session doesn't follow the thread to another.
* A quote picked in one session doesn't follow the thread to another,
* and neither does a message still on its way to the old one.
*/
useEffect(() => {
setReplyTo(null);
setPendingMessages([]);
}, [session_id]);
const bots = useLiveBots({
@@ -87,18 +99,25 @@ export default function Conversation() {
task: pageProps?.bot_task || null,
});
const thread_messages = useMemo(
() => [...messages, ...pending_messages],
[messages, pending_messages],
);
const last_saved_id = messages[messages.length - 1]?.id || null;
const last_thread_message = thread_messages[thread_messages.length - 1];
const awaiting_reply = last_thread_message?.author == "user";
const reply_progress = useReplyProgress({
bot_id: bot?.id || 0,
session_id,
last_message_id: messages[messages.length - 1]?.id || null,
last_message_id: last_saved_id,
});
const awaiting_reply = messages[messages.length - 1]?.author == "user";
useReplyCatchUp({
session_id,
awaiting_reply,
last_message_id: messages[messages.length - 1]?.id || null,
last_message_id: last_saved_id,
});
/**
@@ -117,6 +136,14 @@ export default function Conversation() {
if (message.bot_id != bot?.id) return;
if ((message.workspace_id || null) != workspace_id) return;
/**
* A pushed copy of a message sent here (its response can come
* after the push) takes the pending bubble's place.
*/
setPendingMessages((prev) =>
dropSentMessage({ pending: prev, message }),
);
if (message.session_id && message.session_id != session_id) {
if (message.author == "user" && startsNewSession(message)) {
setSessionId(Number(message.session_id));
@@ -207,9 +234,10 @@ export default function Conversation() {
/>
<MessageThread
bot={live_bot}
messages={messages}
messages={thread_messages}
progress={progress.text}
reply_progress={reply_progress}
receiving={pending_messages.length > 0}
has_task={Boolean(progress.task_id)}
onQuote={({ text }) =>
composer.current?.insertQuote({ text })
@@ -223,8 +251,28 @@ export default function Conversation() {
voice={pageProps?.bot_voice}
reply_to={reply_to}
onCancelReply={() => setReplyTo(null)}
onSendStart={({ message }) =>
setPendingMessages((prev) => [
...prev,
message,
])
}
onSendFailed={({ temp_id }) =>
setPendingMessages((prev) =>
prev.filter(
(message) =>
message.id != temp_id,
),
)
}
onSent={({ message }) => {
setReplyTo(null);
setPendingMessages((prev) =>
dropSentMessage({
pending: prev,
message,
}),
);
if (
startsNewSession(message) &&