Compare commits

...

2 Commits

Author SHA1 Message Date
肖应宇 08e8de5e3e fix(workspace): 恢复先删后建并修复新会话初始化时序
- 新会话初始化改为 delete -> create

- 通过初始化就绪门控,确保 history 在创建完成后再加载

- 发送消息前等待初始化完成,避免与初始化并发
2026-04-28 18:34:18 +08:00
肖应宇 d1cdb7eef7 fix(workspace): 调整输入区与消息展示逻辑
- 输入框新增返回欢迎页按钮

- 人类消息展示保留原始换行

- 调整引用刷新策略与中英文文案
2026-04-28 18:29:39 +08:00
8 changed files with 109 additions and 40 deletions

View File

@ -81,16 +81,23 @@ export default function ChatPage() {
() => isNewThread && !safeThreadId, () => isNewThread && !safeThreadId,
[isNewThread, safeThreadId], [isNewThread, safeThreadId],
); );
const [isThreadInitReady, setIsThreadInitReady] = useState(false);
const streamThreadId = useMemo(() => { const streamThreadId = useMemo(() => {
if (isNewThread && createNewSession) { if (!safeThreadId) {
return undefined;
}
// In /new flow, defer history loading until thread init is finished:
// delete -> create -> history.
if (isNewThread && !isThreadInitReady) {
return undefined; return undefined;
} }
return safeThreadId; return safeThreadId;
}, [createNewSession, isNewThread, safeThreadId]); }, [isNewThread, isThreadInitReady, safeThreadId]);
const apiClient = useMemo(() => getAPIClient(isMock), [isMock]); const apiClient = useMemo(() => getAPIClient(isMock), [isMock]);
const warnedMissingThreadIdRef = useRef(false); const warnedMissingThreadIdRef = useRef(false);
const initializedThreadRef = useRef<string | null>(null); const initializedThreadRef = useRef<string | null>(null);
const threadInitPromiseRef = useRef<Promise<void> | null>(null);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const currentSlogan = motivationSlogans[ const currentSlogan = motivationSlogans[
@ -130,6 +137,7 @@ export default function ChatPage() {
useEffect(() => { useEffect(() => {
if (!isNewThread) { if (!isNewThread) {
warnedMissingThreadIdRef.current = false; warnedMissingThreadIdRef.current = false;
setIsThreadInitReady(true);
return; return;
} }
if (!safeThreadId) { if (!safeThreadId) {
@ -137,29 +145,38 @@ export default function ChatPage() {
warnedMissingThreadIdRef.current = true; warnedMissingThreadIdRef.current = true;
toast.error(t.chatPage.missingThreadIdForCreate); toast.error(t.chatPage.missingThreadIdForCreate);
} }
setIsThreadInitReady(false);
return; return;
} }
warnedMissingThreadIdRef.current = false; warnedMissingThreadIdRef.current = false;
if (initializedThreadRef.current === safeThreadId) return; if (initializedThreadRef.current === safeThreadId) return;
initializedThreadRef.current = safeThreadId; initializedThreadRef.current = safeThreadId;
void apiClient.threads setIsThreadInitReady(false);
// TODO: 先注释先删除再创建的逻辑
// .delete(safeThreadId) const initPromise = apiClient.threads
// .catch(() => undefined) .delete(safeThreadId)
// .then(() => .catch(() => undefined)
// apiClient.threads.create({ .then(() =>
// threadId: safeThreadId, apiClient.threads.create({
// ifExists: "raise", threadId: safeThreadId,
// }), ifExists: "do_nothing",
// ) }),
.create({ )
threadId: safeThreadId, .then(() => {
ifExists: "do_nothing", setIsThreadInitReady(true);
}) })
.catch(() => { .catch(() => {
initializedThreadRef.current = null; initializedThreadRef.current = null;
setIsThreadInitReady(false);
toast.error(t.chatPage.createSessionFailed); toast.error(t.chatPage.createSessionFailed);
}); });
threadInitPromiseRef.current = initPromise;
void initPromise.finally(() => {
if (threadInitPromiseRef.current === initPromise) {
threadInitPromiseRef.current = null;
}
});
}, [ }, [
apiClient, apiClient,
isNewThread, isNewThread,
@ -291,7 +308,7 @@ export default function ChatPage() {
const [showExitDialog, setShowExitDialog] = useState(false); const [showExitDialog, setShowExitDialog] = useState(false);
const isStreaming = isUploading || thread.isLoading; const isStreaming = isUploading || thread.isLoading;
const handleSubmit = useCallback( const handleSubmit = useCallback(
(message: Parameters<typeof sendMessage>[1]) => { async (message: Parameters<typeof sendMessage>[1]) => {
if (isSelectedSkillBootstrapping) { if (isSelectedSkillBootstrapping) {
return; return;
} }
@ -299,6 +316,12 @@ export default function ChatPage() {
toast.error(t.chatPage.missingThreadIdForSend); toast.error(t.chatPage.missingThreadIdForSend);
return; return;
} }
if (isNewThread && safeThreadId) {
await threadInitPromiseRef.current;
}
if (isNewThread && safeThreadId && !isThreadInitReady) {
return;
}
setHasSubmitted(true); setHasSubmitted(true);
if (safeThreadId && (isNewThread || showWelcomeStyle)) { if (safeThreadId && (isNewThread || showWelcomeStyle)) {
router.replace(`/workspace/chats/${safeThreadId}?is_chatting=true`); router.replace(`/workspace/chats/${safeThreadId}?is_chatting=true`);
@ -307,6 +330,7 @@ export default function ChatPage() {
}, },
[ [
isNewThread, isNewThread,
isThreadInitReady,
isSelectedSkillBootstrapping, isSelectedSkillBootstrapping,
router, router,
safeThreadId, safeThreadId,
@ -633,14 +657,14 @@ export default function ChatPage() {
type: POST_MESSAGE_TYPES.IS_CHATTING, type: POST_MESSAGE_TYPES.IS_CHATTING,
isChatting: false, isChatting: false,
}); });
resetNewSessionState();
// 始终复用 query 中的 thread_id。 // 始终复用 query 中的 thread_id。
const nextQuery = new URLSearchParams(); const nextQuery = new URLSearchParams();
if (threadId && threadId !== "new") { if (threadId && threadId !== "new") {
nextQuery.set("thread_id", threadId); nextQuery.set("thread_id", threadId);
} }
// /workspace/chats/${threadId}?is_chatting=false
router.replace( router.replace(
`/workspace/chats/${threadId}?is_chatting=false`, `/workspace/chats/new?thread_id=${threadId}`,
); );
}} }}
> >

View File

@ -970,6 +970,14 @@ export function InputBox({
/> />
</div> </div>
)} )}
{!showWelcomeStyle && (
<div className="shrink-0 h-full">
<ExitChattingButton
router={router}
threadId={threadIdFromProps}
/>
</div>
)}
<div ref={attachmentsButtonTourRef} className="shrink-0 h-full"> <div ref={attachmentsButtonTourRef} className="shrink-0 h-full">
<AddAttachmentsButton /> <AddAttachmentsButton />
</div> </div>
@ -1292,6 +1300,53 @@ function HistoryButton({
</Tooltip> </Tooltip>
); );
} }
function ExitChattingButton({
className,
router,
threadId,
}: {
className?: string;
router: AppRouterInstance;
threadId: string;
}) {
const { t } = useI18n();
return (
<Tooltip content={t.inputBox.welcome}>
<WorkspaceToolButton
className={cn(
"text-ws-base-1 hover:text-ws-interactive-primary",
className,
)}
onClick={() =>
router.replace(`/workspace/chats/${threadId}?is_chatting=false`)
}
>
<svg
className="transition-[color] duration-200"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="stroke-current transition-[stroke] duration-200"
cx="9"
cy="9"
r="8.5"
/>
<path
className="stroke-current transition-[stroke] duration-200"
d="M6 9H12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</WorkspaceToolButton>
</Tooltip>
);
}
// 启动iframeSkillDialog // 启动iframeSkillDialog
function IframeSkillDialogButton({ function IframeSkillDialogButton({
className, className,

View File

@ -39,7 +39,6 @@ import {
} from "@/core/messages/utils"; } from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { materializeSkillYaml } from "@/core/skills"; import { materializeSkillYaml } from "@/core/skills";
import { humanMessagePlugins } from "@/core/streamdown";
import { dispatchMentionReference } from "@/core/threads/reference-events"; import { dispatchMentionReference } from "@/core/threads/reference-events";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -225,13 +224,9 @@ function MessageContent_({
if (isHuman) { if (isHuman) {
const shouldRenderSummaryCollapse = isSummaryMessage && summaryBody; const shouldRenderSummaryCollapse = isSummaryMessage && summaryBody;
const messageResponse = contentToDisplay ? ( const messageResponse = contentToDisplay ? (
<AIElementMessageResponse <div className="whitespace-break-spaces break-words">
remarkPlugins={humanMessagePlugins.remarkPlugins}
rehypePlugins={humanMessagePlugins.rehypePlugins}
components={components}
>
{contentToDisplay} {contentToDisplay}
</AIElementMessageResponse> </div>
) : null; ) : null;
return ( return (
<div className={cn("ml-auto flex flex-col gap-2", className)}> <div className={cn("ml-auto flex flex-col gap-2", className)}>
@ -250,13 +245,9 @@ function MessageContent_({
: t.toolCalls.expandContent} : t.toolCalls.expandContent}
</summary> </summary>
<AIElementMessageContent className="w-fit border-t"> <AIElementMessageContent className="w-fit border-t">
<AIElementMessageResponse <div className="whitespace-break-spaces break-words">
remarkPlugins={humanMessagePlugins.remarkPlugins}
rehypePlugins={humanMessagePlugins.rehypePlugins}
components={components}
>
{summaryBody} {summaryBody}
</AIElementMessageResponse> </div>
</AIElementMessageContent> </AIElementMessageContent>
</details> </details>
)} )}

View File

@ -32,7 +32,6 @@ export function useReferenceFiles(threadId: string | undefined) {
queryKey: ["references", "list", threadId], queryKey: ["references", "list", threadId],
queryFn: () => listReferenceFiles(threadId ?? ""), queryFn: () => listReferenceFiles(threadId ?? ""),
enabled: Boolean(threadId), enabled: Boolean(threadId),
refetchInterval: 5000, refetchOnWindowFocus: false,
refetchOnWindowFocus: true,
}); });
} }

View File

@ -86,6 +86,7 @@ export const enUS: Translations = {
"Please note, this feature will consume tokens. Ensure your account balance is greater than 200 credits.", "Please note, this feature will consume tokens. Ensure your account balance is greater than 200 credits.",
addAttachments: "Add attachments", addAttachments: "Add attachments",
history: "History", history: "History",
welcome:"Welcome",
selectSkill: "Select Skill", selectSkill: "Select Skill",
mode: "Mode", mode: "Mode",
flashMode: "Flash", flashMode: "Flash",

View File

@ -75,6 +75,7 @@ export interface Translations {
createSkillPrompt: string; createSkillPrompt: string;
addAttachments: string; addAttachments: string;
history: string; history: string;
welcome:string;
selectSkill: string; selectSkill: string;
mode: string; mode: string;
flashMode: string; flashMode: string;

View File

@ -87,6 +87,7 @@ export const zhCN: Translations = {
"请注意此功能将消耗token请保证账户余额大于200可学豆。", "请注意此功能将消耗token请保证账户余额大于200可学豆。",
addAttachments: "添加附件", addAttachments: "添加附件",
history: "历史记录", history: "历史记录",
welcome:"欢迎页",
selectSkill: "选择Skill", selectSkill: "选择Skill",
mode: "模式", mode: "模式",
flashMode: "闪速", flashMode: "闪速",
@ -262,7 +263,7 @@ export const zhCN: Translations = {
noArtifactSelectedDescription: "请选择一个生成文件以查看详情", noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
exitDialogTitle: "提示", exitDialogTitle: "提示",
exitDialogDescription: exitDialogDescription:
"历史记录每七天自动删除,现在将返回欢迎页,是否继续?", "每七天自动删除。现在将返回欢迎页且清空聊天消息,是否继续?",
exitDialogConfirm: "确定", exitDialogConfirm: "确定",
selectedSkillLoadFailed: "技能加载失败", selectedSkillLoadFailed: "技能加载失败",
unknownErrorRetry: "发生了未知错误,请稍后重试。", unknownErrorRetry: "发生了未知错误,请稍后重试。",

View File

@ -414,12 +414,9 @@ export function stripPriorityHintSuffix(content: string): string {
* - Split Chinese-numbered items (e.g. "1...") into separate paragraphs. * - Split Chinese-numbered items (e.g. "1...") into separate paragraphs.
*/ */
export function normalizeHumanMessageDisplayText(content: string): string { export function normalizeHumanMessageDisplayText(content: string): string {
return content // Preserve human input as-is for display; only decode escaped newlines
.replace(/\\n/g, "\n") // and normalize CRLF/CR to LF so line breaks render consistently.
.replace(/\r\n?/g, "\n") return content.replace(/\\n/g, "\n").replace(/\r\n?/g, "\n");
.replace(/\n(?=\d+[)]\s*)/g, "\n\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
} }
export function parseUploadedFiles(content: string): FileInMessage[] { export function parseUploadedFiles(content: string): FileInMessage[] {