fix: 收紧threadId的类型为string,删除无用isNewRoute变量,合并状态判断showInputBox至showWelcomeStyle
This commit is contained in:
parent
7012693802
commit
1606d79bcb
|
|
@ -61,7 +61,6 @@ export default function ChatPage() {
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
isMock,
|
isMock,
|
||||||
showWelcomeStyle,
|
showWelcomeStyle,
|
||||||
invalidNewRoute,
|
|
||||||
} = useThreadChat();
|
} = useThreadChat();
|
||||||
|
|
||||||
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/xclaw_used 参数。
|
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/xclaw_used 参数。
|
||||||
|
|
@ -119,7 +118,6 @@ export default function ChatPage() {
|
||||||
}, [thread.values?.title]);
|
}, [thread.values?.title]);
|
||||||
|
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
const showInputBox = !invalidNewRoute && !(showWelcomeStyle && thread.isThreadLoading);
|
|
||||||
const [historyCutoff, setHistoryCutoff] = useState<number | null>(null);
|
const [historyCutoff, setHistoryCutoff] = useState<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -214,12 +212,10 @@ export default function ChatPage() {
|
||||||
setArtifactsOpen,
|
setArtifactsOpen,
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
]);
|
]);
|
||||||
// shouldRenderHistory || historyCutoff === null
|
|
||||||
// console.log('shouldRenderHistory', shouldRenderHistory, 'historyCutoff', historyCutoff);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ threadId,thread }}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
||||||
|
|
@ -313,43 +309,22 @@ export default function ChatPage() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex size-full justify-center">
|
<div className="flex size-full justify-center">
|
||||||
{invalidNewRoute ? (
|
<MessageList
|
||||||
<div className="flex size-full items-center justify-center px-6">
|
className={cn(
|
||||||
<div
|
"size-full",
|
||||||
className="max-w-md rounded-2xl border border-[#E5DDF2] bg-white px-6 py-5 text-center shadow-sm"
|
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
|
||||||
data-testid="missing-thread-id-state"
|
)}
|
||||||
role="alert"
|
threadId={threadId}
|
||||||
>
|
thread={thread}
|
||||||
<h2 className="text-base font-semibold text-[#150033]">
|
messagesOverride={
|
||||||
缺少 thread_id 参数
|
shouldRenderHistory || historyCutoff === null
|
||||||
</h2>
|
? undefined
|
||||||
<p className="mt-2 text-sm text-[#666666]">
|
: thread.messages.slice(historyCutoff)
|
||||||
访问
|
}
|
||||||
<span className="mx-1 font-mono">/workspace/chats/new</span>
|
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||||
时必须显式传入
|
showScrollToBottomButton={!showWelcomeStyle}
|
||||||
<span className="mx-1 font-mono">?thread_id=...</span>
|
scrollButtonClassName="bottom-[112px]"
|
||||||
,当前页面不会继续使用本地缓存兜底。
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<MessageList
|
|
||||||
className={cn(
|
|
||||||
"size-full",
|
|
||||||
(!showWelcomeStyle || hasSubmitted) && "pt-[58px]",
|
|
||||||
)}
|
|
||||||
threadId={threadId}
|
|
||||||
thread={thread}
|
|
||||||
messagesOverride={
|
|
||||||
shouldRenderHistory || historyCutoff === null
|
|
||||||
? undefined
|
|
||||||
: thread.messages.slice(historyCutoff)
|
|
||||||
}
|
|
||||||
paddingBottom={todoListCollapsed ? 160 : 280}
|
|
||||||
showScrollToBottomButton={!showWelcomeStyle}
|
|
||||||
scrollButtonClassName="bottom-[112px]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -433,7 +408,7 @@ export default function ChatPage() {
|
||||||
showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showInputBox ? (
|
{!(showWelcomeStyle && thread.isThreadLoading) ? (
|
||||||
<InputBox
|
<InputBox
|
||||||
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export function ArtifactFileDetail({
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
filepath: string;
|
filepath: string;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||||
|
|
@ -511,7 +511,7 @@ export function ArtifactFilePreview({
|
||||||
content: string;
|
content: string;
|
||||||
language: string;
|
language: string;
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const zoomScale = zoom / 100;
|
const zoomScale = zoom / 100;
|
||||||
const normalizedContent = useMemo(() => {
|
const normalizedContent = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function ArtifactFileList({
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
files: string[];
|
files: string[];
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { select: selectArtifact, setOpen } = useArtifacts();
|
const { select: selectArtifact, setOpen } = useArtifacts();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import { resolveThreadQueryIntent } from "@/core/threads/utils";
|
||||||
|
|
||||||
export function useThreadChat() {
|
export function useThreadChat() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams<{ thread_id?: string }>();
|
const params = useParams<{ thread_id: string }>();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const threadIdFromSearchParams = searchParams.get("thread_id")?.trim();
|
||||||
// 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。
|
// 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。
|
||||||
const threadIdFromPathname = (() => {
|
const threadIdFromPathname = (() => {
|
||||||
const parts = pathname.split("?")[0]?.split("/") ?? [];
|
const parts = pathname.split("?")[0]?.split("/") ?? [];
|
||||||
|
|
@ -18,8 +20,11 @@ export function useThreadChat() {
|
||||||
return undefined;
|
return undefined;
|
||||||
})();
|
})();
|
||||||
const rawPathThreadId = params?.thread_id ?? threadIdFromPathname;
|
const rawPathThreadId = params?.thread_id ?? threadIdFromPathname;
|
||||||
|
|
||||||
const isNewRoute = rawPathThreadId === "new";
|
const isNewRoute = rawPathThreadId === "new";
|
||||||
const threadIdFromPath = isNewRoute ? undefined : rawPathThreadId;
|
const threadIdFromPathOrParams:string = isNewRoute
|
||||||
|
? threadIdFromSearchParams?? params.thread_id
|
||||||
|
: params.thread_id;
|
||||||
// console.log("[useThreadChat] pathname", pathname);
|
// console.log("[useThreadChat] pathname", pathname);
|
||||||
// console.log("[useThreadChat] params.thread_id", params?.thread_id);
|
// console.log("[useThreadChat] params.thread_id", params?.thread_id);
|
||||||
// console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname);
|
// console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname);
|
||||||
|
|
@ -33,10 +38,9 @@ export function useThreadChat() {
|
||||||
return isValidThreadId(stored) ? stored : undefined;
|
return isValidThreadId(stored) ? stored : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
// 读取 query 的 thread_id(先用 hook,必要时用 window 兜底)。
|
// 读取 query 的 thread_id(先用 hook,必要时用 window 兜底)。
|
||||||
const readQueryThreadId = () => {
|
const readQueryThreadId = () => {
|
||||||
const fromHook = searchParams.get("thread_id")?.trim();
|
const fromHook = threadIdFromSearchParams;
|
||||||
if (isValidThreadId(fromHook)) {
|
if (isValidThreadId(fromHook)) {
|
||||||
return fromHook;
|
return fromHook;
|
||||||
}
|
}
|
||||||
|
|
@ -65,20 +69,16 @@ export function useThreadChat() {
|
||||||
[queryThreadIdFromParams],
|
[queryThreadIdFromParams],
|
||||||
);
|
);
|
||||||
const intent = resolveThreadQueryIntent({
|
const intent = resolveThreadQueryIntent({
|
||||||
pathThreadId: threadIdFromPath,
|
pathThreadId: threadIdFromPathOrParams,
|
||||||
queryThreadId: queryThreadIdFromParams,
|
queryThreadId: queryThreadIdFromParams,
|
||||||
isNewRoute,
|
isNewRoute,
|
||||||
});
|
});
|
||||||
const { isNewThread: isNewRequested, showWelcomeStyle, invalidNewRoute } = intent;
|
const { isNewThread: isNewRequested, showWelcomeStyle } = intent;
|
||||||
const effectiveThreadIdFromPath =
|
|
||||||
invalidNewRoute
|
|
||||||
? undefined
|
|
||||||
: normalizeThreadId(threadIdFromPath) ??
|
|
||||||
(isNewRoute ? undefined : readStoredThreadId());
|
|
||||||
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
|
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
|
||||||
|
|
||||||
const [threadId, setThreadId] = useState(() => {
|
const [threadId, setThreadId] = useState<string>(() => {
|
||||||
return effectiveThreadIdFromPath ?? undefined;
|
return threadIdFromPathOrParams;
|
||||||
});
|
});
|
||||||
|
|
||||||
// New session is only controlled by `/workspace/chats/new`.
|
// New session is only controlled by `/workspace/chats/new`.
|
||||||
|
|
@ -91,17 +91,14 @@ export function useThreadChat() {
|
||||||
}
|
}
|
||||||
setIsNewThread(isNewRoute);
|
setIsNewThread(isNewRoute);
|
||||||
// Prefer path thread id, fall back to query thread_id when path is /new.
|
// Prefer path thread id, fall back to query thread_id when path is /new.
|
||||||
setThreadId(
|
setThreadId(threadIdFromPathOrParams);
|
||||||
invalidNewRoute ? undefined : normalizeThreadId(threadIdFromPath),
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
invalidNewRoute,
|
|
||||||
isNewRoute,
|
isNewRoute,
|
||||||
normalizeThreadId,
|
normalizeThreadId,
|
||||||
pathname,
|
pathname,
|
||||||
searchParams,
|
searchParams,
|
||||||
threadId,
|
threadId,
|
||||||
threadIdFromPath,
|
threadIdFromPathOrParams,
|
||||||
]);
|
]);
|
||||||
const isMock = searchParams.get("mock") === "true";
|
const isMock = searchParams.get("mock") === "true";
|
||||||
return {
|
return {
|
||||||
|
|
@ -110,7 +107,6 @@ export function useThreadChat() {
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
isMock,
|
isMock,
|
||||||
showWelcomeStyle,
|
showWelcomeStyle,
|
||||||
invalidNewRoute,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import type { AgentThread } from "@/core/threads/types";
|
||||||
import { useThread } from "./messages/context";
|
import { useThread } from "./messages/context";
|
||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
|
|
||||||
export function ExportTrigger({ threadId }: { threadId?: string }) {
|
export function ExportTrigger({ threadId }: { threadId: string }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { thread } = useThread();
|
const { thread } = useThread();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export function InputBox({
|
||||||
...props
|
...props
|
||||||
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
|
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
|
||||||
assistantId?: string | null;
|
assistantId?: string | null;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
status?: ChatStatus;
|
status?: ChatStatus;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
context: Omit<
|
context: Omit<
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { AgentThreadState } from "@/core/threads";
|
||||||
|
|
||||||
export interface ThreadContextType {
|
export interface ThreadContextType {
|
||||||
thread: UseStream<AgentThreadState>;
|
thread: UseStream<AgentThreadState>;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
isMock?: boolean;
|
isMock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function MessageListItem({
|
||||||
className?: string;
|
className?: string;
|
||||||
message: Message;
|
message: Message;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const isHuman = message.type === "human";
|
const isHuman = message.type === "human";
|
||||||
return (
|
return (
|
||||||
|
|
@ -94,7 +94,7 @@ function MessageImage({
|
||||||
maxWidth = "90%",
|
maxWidth = "90%",
|
||||||
...props
|
...props
|
||||||
}: React.ImgHTMLAttributes<HTMLImageElement> & {
|
}: React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
|
|
@ -124,7 +124,7 @@ function MessageContent_({
|
||||||
className?: string;
|
className?: string;
|
||||||
message: Message;
|
message: Message;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
const isHuman = message.type === "human";
|
const isHuman = message.type === "human";
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export function MessageList({
|
||||||
scrollButtonClassName,
|
scrollButtonClassName,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
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[];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export function ThreadTitle({
|
||||||
threadTitle,
|
threadTitle,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
thread?: UseStream<AgentThreadState>;
|
thread?: UseStream<AgentThreadState>;
|
||||||
threadTitle?: string;
|
threadTitle?: string;
|
||||||
}) {
|
}) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function useArtifactContent({
|
||||||
enabled,
|
enabled,
|
||||||
}: {
|
}: {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isWriteFile = useMemo(() => {
|
const isWriteFile = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,4 @@ void test("prefers path thread id over query thread id when not on /new", () =>
|
||||||
|
|
||||||
assert.equal(intent.isNewThread, false);
|
assert.equal(intent.isNewThread, false);
|
||||||
assert.equal(intent.threadId, "thread-from-path");
|
assert.equal(intent.threadId, "thread-from-path");
|
||||||
assert.equal(intent.invalidNewRoute, false);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export type ToolEndEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThreadStreamOptions = {
|
export type ThreadStreamOptions = {
|
||||||
threadId?: string | null | undefined;
|
threadId: string | null | undefined;
|
||||||
context: LocalSettings["context"];
|
context: LocalSettings["context"];
|
||||||
createNewSession?: boolean;
|
createNewSession?: boolean;
|
||||||
isMock?: boolean;
|
isMock?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ export interface ThreadQueryIntent {
|
||||||
threadId: string | undefined;
|
threadId: string | undefined;
|
||||||
isNewThread: boolean;
|
isNewThread: boolean;
|
||||||
showWelcomeStyle: boolean;
|
showWelcomeStyle: boolean;
|
||||||
invalidNewRoute: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pathOfThread(threadId: string) {
|
export function pathOfThread(threadId: string) {
|
||||||
|
|
@ -45,8 +44,6 @@ export function resolveThreadQueryIntent({
|
||||||
// 新逻辑只由路由 /workspace/chats/new 控制“新会话”
|
// 新逻辑只由路由 /workspace/chats/new 控制“新会话”
|
||||||
isNewThread,
|
isNewThread,
|
||||||
showWelcomeStyle: isNewThread,
|
showWelcomeStyle: isNewThread,
|
||||||
// 新逻辑下不再要求 /new 必带 query thread_id
|
|
||||||
invalidNewRoute: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ interface SkillError {
|
||||||
|
|
||||||
interface UseSelectedSkillListenerOptions {
|
interface UseSelectedSkillListenerOptions {
|
||||||
/** 当前会话 thread_id,用于调用 bootstrapRemoteSkill */
|
/** 当前会话 thread_id,用于调用 bootstrapRemoteSkill */
|
||||||
threadId?: string | null;
|
threadId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseSelectedSkillListenerReturn {
|
interface UseSelectedSkillListenerReturn {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function buildChatUrl({
|
||||||
}: {
|
}: {
|
||||||
pathThreadId?: string;
|
pathThreadId?: string;
|
||||||
xclawUsed: boolean;
|
xclawUsed: boolean;
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const resolvedThreadId = threadId ?? pathThreadId;
|
const resolvedThreadId = threadId ?? pathThreadId;
|
||||||
if (!pathThreadId && !resolvedThreadId) {
|
if (!pathThreadId && !resolvedThreadId) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue