Load a conversation's history on the client
The conversation pages rendered their messages on the server, so a page the back button restored from the back-forward cache showed an old thread. The server now renders everything else (rail, header, composer) and the history (active session, latest messages, active task) loads from api/admin/bots/conversation-history once the page is on screen, with a loading line under the header, and again on a pageshow restore. Messages sent or pushed while it loads are merged with it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ConversationHistory } from "@/src/types";
|
||||
import fetchConversationHistory from "../(functions)/fetch-conversation-history";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
workspace_id: number | null;
|
||||
/**
|
||||
* Takes each load: the thread starts over from it.
|
||||
*/
|
||||
onLoaded: (params: { history: ConversationHistory }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the open conversation's history once the page is on screen, and
|
||||
* again whenever the browser restores the page from its back-forward
|
||||
* cache (`pageshow` with `persisted`), so going back never shows an old
|
||||
* thread. Everything else on the page (the header, the composer) comes
|
||||
* from the server and shows at once. `loaded` turns true with the first
|
||||
* load; a conversation the user can no longer open sends them where the
|
||||
* server says.
|
||||
*/
|
||||
export default function useConversationHistory({ bot_id, workspace_id, onLoaded }: Params) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const request = useRef(0);
|
||||
const on_loaded = useRef(onLoaded);
|
||||
|
||||
on_loaded.current = onLoaded;
|
||||
|
||||
const load = async () => {
|
||||
if (!bot_id) return;
|
||||
|
||||
const id = ++request.current;
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await fetchConversationHistory({ bot_id, workspace_id }).catch(() => null);
|
||||
|
||||
/** A newer load started meanwhile: it has the last word. */
|
||||
if (id != request.current) return;
|
||||
|
||||
if (res?.success && res.singleRes?.redirect) {
|
||||
window.location.replace(res.singleRes.redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
const history = res?.success ? res.singleRes?.history : null;
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!history?.session?.id) {
|
||||
setError(res?.msg || "Couldn't load the conversation.");
|
||||
return;
|
||||
}
|
||||
|
||||
on_loaded.current({ history });
|
||||
setLoaded(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
|
||||
const onPageShow = (event: PageTransitionEvent) => {
|
||||
if (event.persisted) load();
|
||||
};
|
||||
|
||||
window.addEventListener("pageshow", onPageShow);
|
||||
return () => window.removeEventListener("pageshow", onPageShow);
|
||||
}, [bot_id, workspace_id]);
|
||||
|
||||
return { loading, loaded, error, retry: load };
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import postSessionApi from "@/src/functions/frontend/post-session-api";
|
||||
import type { BotMessageWithAttachments, OlderBotMessagesData } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
session_id?: number | null;
|
||||
/**
|
||||
* The page rendered with only the session's latest messages.
|
||||
* The thread starts with only the session's latest messages.
|
||||
*/
|
||||
has_more: boolean;
|
||||
/**
|
||||
@@ -21,7 +21,8 @@ type Params = {
|
||||
/**
|
||||
* Loads the session's older messages a page at a time, when the thread
|
||||
* is scrolled to its top. A session the thread moves onto starts with
|
||||
* its first message, so it has none older.
|
||||
* its first message, so it has none older; `reset` starts over from a
|
||||
* loaded history instead.
|
||||
*/
|
||||
export default function useOlderMessages({
|
||||
session_id,
|
||||
@@ -32,15 +33,28 @@ export default function useOlderMessages({
|
||||
const [has_more, setHasMore] = useState(initial_has_more);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const first_session = useRef(session_id);
|
||||
const base_session = useRef(session_id);
|
||||
|
||||
useEffect(() => {
|
||||
if (session_id == first_session.current) return;
|
||||
if (session_id == base_session.current) return;
|
||||
|
||||
base_session.current = session_id;
|
||||
setHasMore(false);
|
||||
setError(undefined);
|
||||
}, [session_id]);
|
||||
|
||||
/**
|
||||
* A history loaded for the session: whether it has older messages.
|
||||
*/
|
||||
const reset = useCallback(
|
||||
({ session_id, has_more }: { session_id: number; has_more: boolean }) => {
|
||||
base_session.current = session_id;
|
||||
setHasMore(has_more);
|
||||
setError(undefined);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const loadOlder = async () => {
|
||||
if (loading || !has_more || !session_id || !first_message_id) return;
|
||||
|
||||
@@ -63,5 +77,5 @@ export default function useOlderMessages({
|
||||
setHasMore(res.singleRes.has_more);
|
||||
};
|
||||
|
||||
return { has_more, loading, error, loadOlder };
|
||||
return { has_more, loading, error, loadOlder, reset };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import useServerEvent from "@/src/hooks/use-server-event";
|
||||
import type { BotJobUpdateData, BotTaskWithSteps, BotUpdatedData } from "@/src/types";
|
||||
|
||||
@@ -9,7 +9,7 @@ type Params = {
|
||||
*/
|
||||
session_id?: number | null;
|
||||
/**
|
||||
* The bot's active task when the page loaded.
|
||||
* The bot's active task when the thread started.
|
||||
*/
|
||||
task: BotTaskWithSteps | null;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ function fromTask({ task }: { task: BotTaskWithSteps | null }): Progress {
|
||||
* The open session's active task and its latest step (a
|
||||
* `bots_queue_updates` row's text), kept current from
|
||||
* `server:bot-job-update`. Starts over when `server:bot-updated` brings a
|
||||
* different task, or none.
|
||||
* different task, or none, or `reset` brings a loaded history's.
|
||||
*/
|
||||
export default function useTaskProgress({ bot_id, session_id, task }: Params) {
|
||||
const [progress, setProgress] = useState<Progress>(() => fromTask({ task }));
|
||||
@@ -60,5 +60,9 @@ export default function useTaskProgress({ bot_id, session_id, task }: Params) {
|
||||
},
|
||||
});
|
||||
|
||||
return progress;
|
||||
const reset = useCallback(({ task }: { task: BotTaskWithSteps | null }) => {
|
||||
setProgress(fromTask({ task }));
|
||||
}, []);
|
||||
|
||||
return { ...progress, reset };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user