保持new路由下的空白引导页

This commit is contained in:
Titan 2026-03-12 11:54:43 +08:00
parent 4119fdcba7
commit e2fdfa75d7
3 changed files with 78 additions and 21 deletions

View File

@ -80,20 +80,19 @@ export default function ChatPage() {
}, 100);
}
}, [inputInitialValue]);
const isNewThread = useMemo(
() => {
// UI mode depends only on route: /workspace/chats/new is always "new page" mode.
const isNewThread = useMemo(() => threadIdFromPath === "new", [threadIdFromPath]);
// Submission strategy is controlled by `isnew` query param only.
// - isnew=false: reuse existing thread
// - otherwise: create/start a new session
const createNewSession = useMemo(() => {
if (threadIdFromPath !== "new") {
return false;
}
const queryThreadId = searchParams.get("thread_id")?.trim();
const queryIsNew = searchParams.get("isnew")?.trim().toLowerCase();
const shouldReuseExisting = queryIsNew === "false" && !!queryThreadId;
return !shouldReuseExisting;
},
[threadIdFromPath, searchParams],
);
return searchParams.get("isnew")?.trim().toLowerCase() !== "false";
}, [threadIdFromPath, searchParams]);
const uploadTarget = useMemo(() => {
const target = searchParams.get("upload_target")?.trim().toLowerCase();
@ -110,11 +109,21 @@ export default function ChatPage() {
}
}, [threadIdFromPath, searchParams]);
// Runtime strategy for /new page:
// - UI remains new-page mode
// - if isnew=false, execute against existing thread_id without creating a new one
const reuseExistingThread = useMemo(
() => threadIdFromPath === "new" && !createNewSession && !!threadId,
[threadIdFromPath, createNewSession, threadId],
);
const { showNotification } = useNotification();
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
const thread = useThreadStream({
isNewThread,
// Keep UI in new-page mode, but runtime may reuse existing thread
isNewThread: reuseExistingThread ? false : isNewThread,
threadId,
fetchStateHistory: true,
onFinish: (state) => {
setFinalState(state);
if (document.hidden || !document.hasFocus()) {
@ -150,13 +159,16 @@ export default function ChatPage() {
return result;
}, [thread, isNewThread]);
const [hasSubmitted, setHasSubmitted] = useState(false);
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
useEffect(() => {
const pageTitle = isNewThread
? t.pages.newChat
: thread.values?.title && thread.values.title !== "Untitled"
? thread.values.title
: t.pages.untitled;
if (thread.isThreadLoading) {
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
document.title = `Loading... - ${t.pages.appName}`;
} else {
document.title = `${pageTitle} - ${t.pages.appName}`;
@ -168,6 +180,7 @@ export default function ChatPage() {
t.pages.appName,
thread.values.title,
thread.isThreadLoading,
suppressExistingThreadPrefetchUi,
]);
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
@ -198,8 +211,9 @@ export default function ChatPage() {
const [todoListCollapsed, setTodoListCollapsed] = useState(true);
const handleSubmit = useSubmitThread({
const submitThread = useSubmitThread({
isNewThread,
createNewSession,
threadId,
thread,
uploadTarget,
@ -214,6 +228,13 @@ export default function ChatPage() {
router.push(pathOfThread(threadId!));
},
});
const handleSubmit = useCallback(
(message: Parameters<typeof submitThread>[0]) => {
setHasSubmitted(true);
void submitThread(message);
},
[submitThread],
);
const handleStop = useCallback(async () => {
await thread.stop();
}, [thread]);
@ -268,8 +289,11 @@ export default function ChatPage() {
className={cn("size-full", !isNewThread && "pt-10")}
threadId={threadId}
thread={thread}
suppressThreadLoading={suppressExistingThreadPrefetchUi}
messagesOverride={
!thread.isLoading && finalState?.messages
suppressExistingThreadPrefetchUi
? []
: !thread.isLoading && finalState?.messages
? (finalState.messages as Message[])
: undefined
}
@ -306,7 +330,13 @@ export default function ChatPage() {
className={cn("bg-background/5 w-full -translate-y-4")}
isNewThread={isNewThread}
autoFocus={isNewThread}
status={thread.isLoading ? "streaming" : "ready"}
status={
suppressExistingThreadPrefetchUi
? "ready"
: thread.isLoading
? "streaming"
: "ready"
}
context={settings.context}
extraHeader={
isNewThread && <Welcome mode={settings.context.mode} />

View File

@ -35,6 +35,7 @@ export function MessageList({
threadId,
thread,
messagesOverride,
suppressThreadLoading = false,
paddingBottom = 160,
}: {
className?: string;
@ -42,13 +43,14 @@ export function MessageList({
thread: UseStream<AgentThreadState>;
/** When set (e.g. from onFinish), use instead of thread.messages so SSE end shows complete state. */
messagesOverride?: Message[];
suppressThreadLoading?: boolean;
paddingBottom?: number;
}) {
const { t } = useI18n();
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
const updateSubtask = useUpdateSubtask();
const messages = messagesOverride ?? thread.messages;
if (thread.isThreadLoading) {
if (thread.isThreadLoading && !suppressThreadLoading) {
return <MessageListSkeleton />;
}
return (

View File

@ -21,10 +21,12 @@ import type {
export function useThreadStream({
threadId,
isNewThread,
fetchStateHistory = true,
onFinish,
}: {
isNewThread: boolean;
threadId: string | null | undefined;
fetchStateHistory?: boolean;
onFinish?: (state: AgentThreadState) => void;
}) {
const queryClient = useQueryClient();
@ -34,7 +36,7 @@ export function useThreadStream({
assistantId: "lead_agent",
threadId: isNewThread ? undefined : threadId,
reconnectOnMount: true,
fetchStateHistory: true,
fetchStateHistory,
onCustomEvent(event: unknown) {
console.info(event);
if (
@ -84,10 +86,12 @@ export function useSubmitThread({
thread,
threadContext,
isNewThread,
createNewSession,
uploadTarget,
afterSubmit,
}: {
isNewThread: boolean;
createNewSession: boolean;
threadId: string | null | undefined;
thread: UseStream<AgentThreadState>;
threadContext: Omit<AgentThreadContext, "thread_id">;
@ -95,10 +99,21 @@ export function useSubmitThread({
afterSubmit?: () => void;
}) {
const queryClient = useQueryClient();
const apiClient = getAPIClient();
const callback = useCallback(
async (message: PromptInputMessage) => {
const text = message.text.trim();
// For "new session" semantics, ensure the target thread id starts fresh.
// If the same id already exists, delete it first and let submit recreate it.
if (createNewSession && threadId) {
try {
await apiClient.threads.delete(threadId);
} catch {
// Ignore delete errors (e.g. thread does not exist yet)
}
}
// Upload files first if any
if (message.files && message.files.length > 0) {
try {
@ -154,7 +169,7 @@ export function useSubmitThread({
] as HumanMessage[],
},
{
threadId: isNewThread ? threadId! : undefined,
threadId: createNewSession ? threadId! : undefined,
streamSubgraphs: true,
streamResumable: true,
streamMode: ["values", "messages-tuple", "custom"],
@ -170,7 +185,17 @@ export function useSubmitThread({
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
afterSubmit?.();
},
[thread, isNewThread, threadId, threadContext, uploadTarget, queryClient, afterSubmit],
[
thread,
isNewThread,
createNewSession,
threadId,
threadContext,
uploadTarget,
queryClient,
apiClient,
afterSubmit,
],
);
return callback;
}