保持new路由下的空白引导页
This commit is contained in:
parent
4119fdcba7
commit
e2fdfa75d7
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue