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

View File

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

View File

@ -21,10 +21,12 @@ import type {
export function useThreadStream({ export function useThreadStream({
threadId, threadId,
isNewThread, isNewThread,
fetchStateHistory = true,
onFinish, onFinish,
}: { }: {
isNewThread: boolean; isNewThread: boolean;
threadId: string | null | undefined; threadId: string | null | undefined;
fetchStateHistory?: boolean;
onFinish?: (state: AgentThreadState) => void; onFinish?: (state: AgentThreadState) => void;
}) { }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -34,7 +36,7 @@ export function useThreadStream({
assistantId: "lead_agent", assistantId: "lead_agent",
threadId: isNewThread ? undefined : threadId, threadId: isNewThread ? undefined : threadId,
reconnectOnMount: true, reconnectOnMount: true,
fetchStateHistory: true, fetchStateHistory,
onCustomEvent(event: unknown) { onCustomEvent(event: unknown) {
console.info(event); console.info(event);
if ( if (
@ -84,10 +86,12 @@ export function useSubmitThread({
thread, thread,
threadContext, threadContext,
isNewThread, isNewThread,
createNewSession,
uploadTarget, uploadTarget,
afterSubmit, afterSubmit,
}: { }: {
isNewThread: boolean; isNewThread: boolean;
createNewSession: boolean;
threadId: string | null | undefined; threadId: string | null | undefined;
thread: UseStream<AgentThreadState>; thread: UseStream<AgentThreadState>;
threadContext: Omit<AgentThreadContext, "thread_id">; threadContext: Omit<AgentThreadContext, "thread_id">;
@ -95,10 +99,21 @@ export function useSubmitThread({
afterSubmit?: () => void; afterSubmit?: () => void;
}) { }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiClient = getAPIClient();
const callback = useCallback( const callback = useCallback(
async (message: PromptInputMessage) => { async (message: PromptInputMessage) => {
const text = message.text.trim(); 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 // Upload files first if any
if (message.files && message.files.length > 0) { if (message.files && message.files.length > 0) {
try { try {
@ -154,7 +169,7 @@ export function useSubmitThread({
] as HumanMessage[], ] as HumanMessage[],
}, },
{ {
threadId: isNewThread ? threadId! : undefined, threadId: createNewSession ? threadId! : undefined,
streamSubgraphs: true, streamSubgraphs: true,
streamResumable: true, streamResumable: true,
streamMode: ["values", "messages-tuple", "custom"], streamMode: ["values", "messages-tuple", "custom"],
@ -170,7 +185,17 @@ export function useSubmitThread({
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
afterSubmit?.(); afterSubmit?.();
}, },
[thread, isNewThread, threadId, threadContext, uploadTarget, queryClient, afterSubmit], [
thread,
isNewThread,
createNewSession,
threadId,
threadContext,
uploadTarget,
queryClient,
apiClient,
afterSubmit,
],
); );
return callback; return callback;
} }