Compare commits
2 Commits
1fd7a5d4f7
...
08e8de5e3e
| Author | SHA1 | Date |
|---|---|---|
|
|
08e8de5e3e | |
|
|
d1cdb7eef7 |
|
|
@ -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}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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: "发生了未知错误,请稍后重试。",
|
||||||
|
|
|
||||||
|
|
@ -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[] {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue