Compare commits
No commits in common. "da2c1f2cdfb6ccf4d7d5b12a53afd47d9f795e35" and "a6759e2e3aa668b8254389e86df7168e842f78d2" have entirely different histories.
da2c1f2cdf
...
a6759e2e3a
|
|
@ -9,8 +9,7 @@ import { detectLocaleServer } from "@/core/i18n/server";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "XClaw",
|
title: "XClaw",
|
||||||
description:
|
description: "Desscriptions of XClawDesscriptions of XClawDesscriptions of XClaw",
|
||||||
"Desscriptions of XClawDesscriptions of XClawDesscriptions of XClaw",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,13 @@ export default function ChatPage() {
|
||||||
setFullscreen: setArtifactsFullscreen,
|
setFullscreen: setArtifactsFullscreen,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
} = useArtifacts();
|
} = useArtifacts();
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock, showWelcomeStyle } =
|
const {
|
||||||
useThreadChat();
|
threadId,
|
||||||
|
isNewThread,
|
||||||
|
setIsNewThread,
|
||||||
|
isMock,
|
||||||
|
showWelcomeStyle,
|
||||||
|
} = useThreadChat();
|
||||||
|
|
||||||
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。
|
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。
|
||||||
const shouldRenderHistory = !showWelcomeStyle;
|
const shouldRenderHistory = !showWelcomeStyle;
|
||||||
|
|
@ -91,9 +96,8 @@ export default function ChatPage() {
|
||||||
const initializedThreadRef = useRef<string | null>(null);
|
const initializedThreadRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const currentSlogan = motivationSlogans[
|
const currentSlogan =
|
||||||
sloganIndex % motivationSlogans.length
|
motivationSlogans[sloganIndex % motivationSlogans.length] ?? {
|
||||||
] ?? {
|
|
||||||
text: "来,一起学习工作吧",
|
text: "来,一起学习工作吧",
|
||||||
color: "#333333",
|
color: "#333333",
|
||||||
};
|
};
|
||||||
|
|
@ -115,12 +119,9 @@ export default function ChatPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (motivationSlogans.length <= 1) return;
|
if (motivationSlogans.length <= 1) return;
|
||||||
|
|
||||||
const timer = window.setInterval(
|
const timer = window.setInterval(() => {
|
||||||
() => {
|
|
||||||
setSloganIndex((prev) => (prev + 1) % motivationSlogans.length);
|
setSloganIndex((prev) => (prev + 1) % motivationSlogans.length);
|
||||||
},
|
}, 10 * 60 * 1000);
|
||||||
10 * 60 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -312,6 +313,7 @@ export default function ChatPage() {
|
||||||
setIsNewThread,
|
setIsNewThread,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ threadId, thread }}>
|
<ThreadContext.Provider value={{ threadId, thread }}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -418,9 +420,7 @@ export default function ChatPage() {
|
||||||
<main
|
<main
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 max-w-full grow flex-col",
|
"flex min-h-0 max-w-full grow flex-col",
|
||||||
showWelcomeStyle && !hasSubmitted
|
showWelcomeStyle && !hasSubmitted ? "bg-white" : "bg-background",
|
||||||
? "bg-white"
|
|
||||||
: "bg-background",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex size-full justify-center">
|
<div className="flex size-full justify-center">
|
||||||
|
|
@ -499,7 +499,7 @@ export default function ChatPage() {
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-0 grow overflow-auto">
|
<main className="min-h-0 grow overflow-auto">
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
className="mb-[207px] max-w-(--container-width-sm) p-4 pt-12"
|
className="max-w-(--container-width-sm) p-4 pt-12 mb-[207px]"
|
||||||
files={thread.values.artifacts ?? []}
|
files={thread.values.artifacts ?? []}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
/>
|
/>
|
||||||
|
|
@ -523,13 +523,10 @@ export default function ChatPage() {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto relative w-full max-w-[720px]",
|
"pointer-events-auto relative w-full max-w-[720px]",
|
||||||
showWelcomeStyle &&
|
showWelcomeStyle && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
||||||
!hasSubmitted &&
|
|
||||||
"-translate-y-[calc(50vh-96px)]",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!(showWelcomeStyle && thread.isThreadLoading) ? (
|
{!(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}
|
||||||
|
|
@ -561,10 +558,9 @@ export default function ChatPage() {
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
// <InputBoxSkeleton />
|
// <InputBoxSkeleton />
|
||||||
""
|
''
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {isSelectedSkillBootstrapping && (
|
{/* {isSelectedSkillBootstrapping && (
|
||||||
|
|
@ -587,7 +583,7 @@ export default function ChatPage() {
|
||||||
<DevDialogTitle>提示</DevDialogTitle>
|
<DevDialogTitle>提示</DevDialogTitle>
|
||||||
</DevDialogHeader>
|
</DevDialogHeader>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
历史记录每七天自动删除,现在将返回欢迎页,是否继续?
|
(测试中:计划销毁但是现在没有销毁) 退出后,当前会话结束并销毁,请先下载保存当前结果!
|
||||||
</p>
|
</p>
|
||||||
<DevDialogFooter>
|
<DevDialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -616,9 +612,7 @@ export default function ChatPage() {
|
||||||
if (threadId && threadId !== "new") {
|
if (threadId && threadId !== "new") {
|
||||||
nextQuery.set("thread_id", threadId);
|
nextQuery.set("thread_id", threadId);
|
||||||
}
|
}
|
||||||
router.replace(
|
router.replace(`/workspace/chats/${threadId}?is_chatting=false`);
|
||||||
`/workspace/chats/${threadId}?is_chatting=false`,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
|
|
|
||||||
|
|
@ -1146,8 +1146,8 @@ export const PromptInputSubmit = ({
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-[40px] w-[140px] rounded-[10px] border-0 font-bold transition-all",
|
"h-[40px] w-[140px] rounded-[10px] border-0 font-bold transition-all",
|
||||||
isDisabled
|
isDisabled
|
||||||
? "cursor-not-allowed !bg-gray-200 text-gray-400"
|
? "cursor-not-allowed !bg-gray-200 text-gray-400":
|
||||||
: "!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]",
|
"!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,7 @@ export function ArtifactFileDetail({
|
||||||
const artifactViewerSrc = useMemo(() => {
|
const artifactViewerSrc = useMemo(() => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}, []);
|
}, []);
|
||||||
const artifactViewerSandbox =
|
const artifactViewerSandbox = "allow-same-origin allow-scripts allow-downloads";
|
||||||
"allow-same-origin allow-scripts allow-downloads";
|
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
|
|
@ -317,10 +316,7 @@ export function ArtifactFileDetail({
|
||||||
dirnamePosix(markdownEntryPath),
|
dirnamePosix(markdownEntryPath),
|
||||||
artifactEntryPath,
|
artifactEntryPath,
|
||||||
);
|
);
|
||||||
refToRelativeZipPath.set(
|
refToRelativeZipPath.set(ref, relativeFromMarkdown || getFileName(artifactEntryPath));
|
||||||
ref,
|
|
||||||
relativeFromMarkdown || getFileName(artifactEntryPath),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addedVirtualPaths.has(virtualPath)) continue;
|
if (addedVirtualPaths.has(virtualPath)) continue;
|
||||||
addedVirtualPaths.add(virtualPath);
|
addedVirtualPaths.add(virtualPath);
|
||||||
|
|
@ -688,6 +684,7 @@ export function ArtifactFileDetail({
|
||||||
{previewable &&
|
{previewable &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" || language === "html") && (
|
||||||
|
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={displayContent}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
|
|
@ -695,9 +692,10 @@ export function ArtifactFileDetail({
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
filepath={filepath}
|
filepath={filepath}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<div className="mb-0 mb-[207px] min-h-full rounded-b-[10px] bg-white p-0">
|
<div className="min-h-full mb-[207px] rounded-b-[10px] bg-white p-0 mb-0">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none py-[20px]"
|
className="size-full resize-none rounded-none border-none py-[20px]"
|
||||||
value={displayContent ?? ""}
|
value={displayContent ?? ""}
|
||||||
|
|
@ -706,16 +704,16 @@ export function ArtifactFileDetail({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile &&
|
{!isCodeFile && (
|
||||||
(artifactPreviewKind === "pdf" ? (
|
artifactPreviewKind === "pdf" ? (
|
||||||
<ArtifactPdfPreview
|
<ArtifactPdfPreview
|
||||||
className="mb-[207px] h-full"
|
className="h-full mb-[207px]"
|
||||||
artifactUrl={artifactUrl}
|
artifactUrl={artifactUrl}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
/>
|
/>
|
||||||
) : isOfficePreviewKind(artifactPreviewKind) ? (
|
) : isOfficePreviewKind(artifactPreviewKind) ? (
|
||||||
<ArtifactOfficePreview
|
<ArtifactOfficePreview
|
||||||
className="mb-[207px] h-full"
|
className="h-full mb-[207px]"
|
||||||
kind={artifactPreviewKind}
|
kind={artifactPreviewKind}
|
||||||
artifactUrl={artifactUrl}
|
artifactUrl={artifactUrl}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
|
|
@ -729,7 +727,8 @@ export function ArtifactFileDetail({
|
||||||
sandbox={artifactViewerSandbox}
|
sandbox={artifactViewerSandbox}
|
||||||
title={`Artifact preview: ${fileName}`}
|
title={`Artifact preview: ${fileName}`}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</ArtifactContent>
|
</ArtifactContent>
|
||||||
</Artifact>
|
</Artifact>
|
||||||
);
|
);
|
||||||
|
|
@ -821,8 +820,7 @@ function resolveReferencedVirtualPath(
|
||||||
function collectMarkdownAssetTargets(markdown: string): Set<string> {
|
function collectMarkdownAssetTargets(markdown: string): Set<string> {
|
||||||
const targets = new Set<string>();
|
const targets = new Set<string>();
|
||||||
const markdownRefRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
const markdownRefRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
||||||
const htmlAttrRegex =
|
const htmlAttrRegex = /<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*(["'])([^"']+)\1/gi;
|
||||||
/<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*(["'])([^"']+)\1/gi;
|
|
||||||
|
|
||||||
for (const match of markdown.matchAll(markdownRefRegex)) {
|
for (const match of markdown.matchAll(markdownRefRegex)) {
|
||||||
const raw = match[1]?.trim();
|
const raw = match[1]?.trim();
|
||||||
|
|
@ -880,7 +878,7 @@ export function ArtifactFilePreview({
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("mb-[207px] w-full bg-white p-[20px]")}
|
className={cn("w-full bg-white mb-[207px] p-[20px]")}
|
||||||
style={{ "--zoom-scale": zoomScale } as CSSProperties}
|
style={{ "--zoom-scale": zoomScale } as CSSProperties}
|
||||||
>
|
>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
|
|
@ -891,6 +889,7 @@ export function ArtifactFilePreview({
|
||||||
{normalizedContent}
|
{normalizedContent}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (language === "html") {
|
if (language === "html") {
|
||||||
|
|
@ -903,6 +902,7 @@ export function ArtifactFilePreview({
|
||||||
sandbox="allow-scripts allow-forms"
|
sandbox="allow-scripts allow-forms"
|
||||||
style={{ zoom: zoomScale }}
|
style={{ zoom: zoomScale }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ const OPEN_MODE = { chat: 60, artifacts: 40 };
|
||||||
const ChatBox: React.FC<{
|
const ChatBox: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
threadId: string | undefined;
|
threadId: string | undefined;
|
||||||
}> = ({ children, threadId }) => {
|
}> = ({
|
||||||
|
children,
|
||||||
|
threadId,
|
||||||
|
}) => {
|
||||||
const { thread } = useThread();
|
const { thread } = useThread();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const threadIdRef = useRef(threadId);
|
const threadIdRef = useRef(threadId);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
export function useThreadChat() {
|
export function useThreadChat() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams<{ thread_id: string }>();
|
const params = useParams<{ thread_id: string }>();
|
||||||
|
|
@ -44,6 +45,7 @@ export function useThreadChat() {
|
||||||
return threadIdFromPathOrParams ?? "";
|
return threadIdFromPathOrParams ?? "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 记住最近一次有效的 thread_id,供下次加载兜底使用。
|
// 记住最近一次有效的 thread_id,供下次加载兜底使用。
|
||||||
if (threadId && threadId !== "new" && typeof window !== "undefined") {
|
if (threadId && threadId !== "new" && typeof window !== "undefined") {
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@ export function IframeTestPanel() {
|
||||||
|
|
||||||
function handleSendSelectSkill() {
|
function handleSendSelectSkill() {
|
||||||
iframeSkill.sendSelectSkill([{ id: "skill_001", name: "测试技能1" }]);
|
iframeSkill.sendSelectSkill([{ id: "skill_001", name: "测试技能1" }]);
|
||||||
addLog(
|
addLog("postMessage → selectedSkills ([{id:'skill_001',name:'测试技能1'}])");
|
||||||
"postMessage → selectedSkills ([{id:'skill_001',name:'测试技能1'}])",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSendSelectSkillArray() {
|
function handleSendSelectSkillArray() {
|
||||||
|
|
@ -189,8 +187,7 @@ export function IframeTestPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && (
|
{!collapsed && <div className="space-y-3 p-3">
|
||||||
<div className="space-y-3 p-3">
|
|
||||||
{/* 当前状态 */}
|
{/* 当前状态 */}
|
||||||
<div className="rounded-lg bg-gray-50 px-3 py-2 text-xs">
|
<div className="rounded-lg bg-gray-50 px-3 py-2 text-xs">
|
||||||
<div className="mb-1 font-semibold text-gray-500">当前状态</div>
|
<div className="mb-1 font-semibold text-gray-500">当前状态</div>
|
||||||
|
|
@ -353,11 +350,7 @@ export function IframeTestPanel() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{ type: "selectedSkill", id: 999999, title: "不存在的技能" },
|
||||||
type: "selectedSkill",
|
|
||||||
id: 999999,
|
|
||||||
title: "不存在的技能",
|
|
||||||
},
|
|
||||||
"*",
|
"*",
|
||||||
);
|
);
|
||||||
addLog(
|
addLog(
|
||||||
|
|
@ -445,8 +438,7 @@ export function IframeTestPanel() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import type { ChatStatus } from "ai";
|
import type { ChatStatus } from "ai";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
|
@ -60,7 +58,9 @@ import {
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Tag } from "@/components/ui/tag";
|
import { Tag } from "@/components/ui/tag";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
|
import type {
|
||||||
|
SelectedSkillPayloadItem,
|
||||||
|
} from "@/core/i18n/locales/types";
|
||||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import type { AgentThreadContext } from "@/core/threads";
|
import type { AgentThreadContext } from "@/core/threads";
|
||||||
|
|
@ -86,7 +86,6 @@ import {
|
||||||
|
|
||||||
import { ModeHoverGuide } from "./mode-hover-guide";
|
import { ModeHoverGuide } from "./mode-hover-guide";
|
||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
|
||||||
|
|
||||||
export function InputBox({
|
export function InputBox({
|
||||||
className,
|
className,
|
||||||
|
|
@ -133,7 +132,7 @@ export function InputBox({
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const iframeSkill = useIframeSkill({ threadId: threadIdFromProps });
|
const iframeSkill = useIframeSkill({ threadId: threadIdFromProps });
|
||||||
const isInputDisabled = (disabled ?? false) || iframeSkill.isBootstrapping;
|
const isInputDisabled = (disabled ?? false) || iframeSkill.isBootstrapping;
|
||||||
const router = useRouter();
|
|
||||||
const threadId = threadIdFromProps;
|
const threadId = threadIdFromProps;
|
||||||
const { textInput } = usePromptInputController();
|
const { textInput } = usePromptInputController();
|
||||||
const attachments = usePromptInputAttachments();
|
const attachments = usePromptInputAttachments();
|
||||||
|
|
@ -376,11 +375,6 @@ export function InputBox({
|
||||||
/>
|
/>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu> */}
|
</PromptInputActionMenu> */}
|
||||||
<HistoryButton
|
|
||||||
className="px-2!"
|
|
||||||
router={router}
|
|
||||||
threadId={threadIdFromProps}
|
|
||||||
/>
|
|
||||||
<AddAttachmentsButton className="px-2!" />
|
<AddAttachmentsButton className="px-2!" />
|
||||||
<IframeSkillDialogButton
|
<IframeSkillDialogButton
|
||||||
className="px-2!"
|
className="px-2!"
|
||||||
|
|
@ -389,7 +383,6 @@ export function InputBox({
|
||||||
openSkillDialog={iframeSkill.openSkillDialog}
|
openSkillDialog={iframeSkill.openSkillDialog}
|
||||||
clearSkill={iframeSkill.clearSkill}
|
clearSkill={iframeSkill.clearSkill}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 参考 kexue 版本隐藏运行模式切换按钮 */}
|
{/* 参考 kexue 版本隐藏运行模式切换按钮 */}
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
{/* <ModelSelector
|
{/* <ModelSelector
|
||||||
|
|
@ -436,9 +429,7 @@ export function InputBox({
|
||||||
/>
|
/>
|
||||||
</PromptInput>
|
</PromptInput>
|
||||||
|
|
||||||
{showWelcomeStyle &&
|
{showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill" && (
|
||||||
!hasSubmitted &&
|
|
||||||
searchParams.get("mode") !== "skill" && (
|
|
||||||
<SuggestionListContainer
|
<SuggestionListContainer
|
||||||
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
|
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
|
||||||
isBootstrapping={iframeSkill.isBootstrapping}
|
isBootstrapping={iframeSkill.isBootstrapping}
|
||||||
|
|
@ -541,19 +532,19 @@ function SuggestionList({
|
||||||
const promptSuggestions = suggestions.filter(
|
const promptSuggestions = suggestions.filter(
|
||||||
(
|
(
|
||||||
suggestion,
|
suggestion,
|
||||||
): suggestion is Exclude<
|
): suggestion is Exclude<(typeof suggestions)[number], { type: "separator" }> =>
|
||||||
(typeof suggestions)[number],
|
!("type" in suggestion),
|
||||||
{ type: "separator" }
|
|
||||||
> => !("type" in suggestion),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSuggestionClick = useCallback(
|
const handleSuggestionClick = useCallback(
|
||||||
(suggestion: {
|
(
|
||||||
|
suggestion: {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
skill_id?: string[];
|
skill_id?: string[];
|
||||||
children?: SelectedSkillPayloadItem[];
|
children?: SelectedSkillPayloadItem[];
|
||||||
suggestion: string;
|
suggestion: string;
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
if (isBootstrapping) return;
|
if (isBootstrapping) return;
|
||||||
|
|
||||||
// 优先使用 children 中的 skill(保留每个 skill 自己的 name,用于 tag 展示)
|
// 优先使用 children 中的 skill(保留每个 skill 自己的 name,用于 tag 展示)
|
||||||
|
|
@ -562,8 +553,7 @@ function SuggestionList({
|
||||||
id: String(item.id).trim(),
|
id: String(item.id).trim(),
|
||||||
name: item.name?.trim() ?? "",
|
name: item.name?.trim() ?? "",
|
||||||
}))
|
}))
|
||||||
.filter(
|
.filter((item): item is { id: string; name: string } =>
|
||||||
(item): item is { id: string; name: string } =>
|
|
||||||
Boolean(item.id) && Boolean(item.name),
|
Boolean(item.id) && Boolean(item.name),
|
||||||
);
|
);
|
||||||
if (childSkills.length > 0) {
|
if (childSkills.length > 0) {
|
||||||
|
|
@ -603,10 +593,7 @@ function SuggestionList({
|
||||||
[bootstrapAndLockSkills, isBootstrapping, textInput],
|
[bootstrapAndLockSkills, isBootstrapping, textInput],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Suggestions
|
<Suggestions className="min-h-16 w-fit items-start" data-testid="welcome-suggestions">
|
||||||
className="min-h-16 w-fit items-start"
|
|
||||||
data-testid="welcome-suggestions"
|
|
||||||
>
|
|
||||||
{promptSuggestions.map((suggestion) => (
|
{promptSuggestions.map((suggestion) => (
|
||||||
<Suggestion
|
<Suggestion
|
||||||
key={suggestion.suggestion}
|
key={suggestion.suggestion}
|
||||||
|
|
@ -649,39 +636,6 @@ function AddAttachmentsButton({ className }: { className?: string }) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryButton({
|
|
||||||
className,
|
|
||||||
router,
|
|
||||||
threadId,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
router: AppRouterInstance;
|
|
||||||
threadId: string;
|
|
||||||
}) {
|
|
||||||
const { t } = useI18n();
|
|
||||||
return (
|
|
||||||
<Tooltip content={t.inputBox.history}>
|
|
||||||
<PromptInputButton
|
|
||||||
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
|
||||||
onClick={() =>
|
|
||||||
router.replace(`/workspace/chats/${threadId}?is_chatting=true`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="[&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
height="24px"
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
width="24px"
|
|
||||||
fill="#1f1f1f"
|
|
||||||
>
|
|
||||||
<path d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z" />
|
|
||||||
</svg>
|
|
||||||
</PromptInputButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 启动iframeSkillDialog
|
// 启动iframeSkillDialog
|
||||||
function IframeSkillDialogButton({
|
function IframeSkillDialogButton({
|
||||||
className,
|
className,
|
||||||
|
|
@ -733,10 +687,7 @@ function IframeSkillDialogButton({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedSkills.map((skill, index) => (
|
{selectedSkills.map((skill, index) => (
|
||||||
<Tag
|
<Tag key={`${skill.skill_id}-${skill.title}-${index}`} className="shrink-0">
|
||||||
key={`${skill.skill_id}-${skill.title}-${index}`}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
{skill.title}
|
{skill.title}
|
||||||
{/* TODO: 因为后端接口不支持取消选择skill,所以暂时禁用取消选择按钮 */}
|
{/* TODO: 因为后端接口不支持取消选择skill,所以暂时禁用取消选择按钮 */}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,9 @@ export function MessageGroup({
|
||||||
return filteredSteps[filteredSteps.length - 1];
|
return filteredSteps[filteredSteps.length - 1];
|
||||||
}
|
}
|
||||||
}, [lastToolCallStep, steps]);
|
}, [lastToolCallStep, steps]);
|
||||||
const totalToolStepCount =
|
const totalToolStepCount = aboveLastToolCallSteps.length + (lastToolCallStep ? 1 : 0);
|
||||||
aboveLastToolCallSteps.length + (lastToolCallStep ? 1 : 0);
|
const shouldShowToolSteps = !!lastToolCallStep &&
|
||||||
const shouldShowToolSteps =
|
(showAbove || aboveLastToolCallSteps.length === 0);
|
||||||
!!lastToolCallStep && (showAbove || aboveLastToolCallSteps.length === 0);
|
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
return (
|
return (
|
||||||
<ChainOfThought
|
<ChainOfThought
|
||||||
|
|
@ -417,15 +416,14 @@ function ToolCall({
|
||||||
return t.toolCalls.executeCommand;
|
return t.toolCalls.executeCommand;
|
||||||
}
|
}
|
||||||
const command: string | undefined = (args as { command: string })?.command;
|
const command: string | undefined = (args as { command: string })?.command;
|
||||||
const shouldCollapse =
|
const shouldCollapse = !!command && command.length > TOOL_CONTENT_COLLAPSE_THRESHOLD;
|
||||||
!!command && command.length > TOOL_CONTENT_COLLAPSE_THRESHOLD;
|
|
||||||
return (
|
return (
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={id}
|
key={id}
|
||||||
label={description}
|
label={description}
|
||||||
icon={SquareTerminalIcon}
|
icon={SquareTerminalIcon}
|
||||||
action={
|
action={shouldCollapse
|
||||||
shouldCollapse ? (
|
? (
|
||||||
<Button
|
<Button
|
||||||
className="h-7 px-3 text-xs"
|
className="h-7 px-3 text-xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -438,14 +436,11 @@ function ToolCall({
|
||||||
? t.toolCalls.collapseContent
|
? t.toolCalls.collapseContent
|
||||||
: t.toolCalls.expandContent}
|
: t.toolCalls.expandContent}
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
)
|
||||||
}
|
: undefined}
|
||||||
>
|
>
|
||||||
{command && (
|
{command && (
|
||||||
<ExpandableToolContent
|
<ExpandableToolContent content={command} expanded={isCommandExpanded} />
|
||||||
content={command}
|
|
||||||
expanded={isCommandExpanded}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ChainOfThoughtStep>
|
</ChainOfThoughtStep>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,7 @@ function MessageImage({
|
||||||
}
|
}
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
src.startsWith("/mnt/") && threadId
|
src.startsWith("/mnt/") && threadId ? resolveArtifactURL(src, threadId) : src;
|
||||||
? resolveArtifactURL(src, threadId)
|
|
||||||
: src;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,7 @@ export function MessageList({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{thread.isLoading && messages.length > 0 && (
|
{thread.isLoading && messages.length > 0 && <StreamingIndicator className="my-4" />}
|
||||||
<StreamingIndicator className="my-4" />
|
|
||||||
)}
|
|
||||||
<div style={{ height: `${paddingBottom}px` }} />
|
<div style={{ height: `${paddingBottom}px` }} />
|
||||||
</ConversationContent>
|
</ConversationContent>
|
||||||
{/* showScrollToBottomButton */}
|
{/* showScrollToBottomButton */}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ export function WorkspaceSidebar({
|
||||||
<WorkspaceNavChatList />
|
<WorkspaceNavChatList />
|
||||||
{isSidebarOpen && <RecentChatList />}
|
{isSidebarOpen && <RecentChatList />}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>{/* <WorkspaceNavMenu /> */}</SidebarFooter>
|
<SidebarFooter>
|
||||||
|
{/* <WorkspaceNavMenu /> */}
|
||||||
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ export const enUS: Translations = {
|
||||||
sendMessagePrice:
|
sendMessagePrice:
|
||||||
"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",
|
|
||||||
selectSkill: "Select Skill",
|
selectSkill: "Select Skill",
|
||||||
mode: "Mode",
|
mode: "Mode",
|
||||||
flashMode: "Flash",
|
flashMode: "Flash",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface SelectedSkillPayloadItem {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Translations {
|
export interface Translations {
|
||||||
// Locale meta
|
// Locale meta
|
||||||
locale: {
|
locale: {
|
||||||
|
|
@ -71,7 +72,6 @@ export interface Translations {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
createSkillPrompt: string;
|
createSkillPrompt: string;
|
||||||
addAttachments: string;
|
addAttachments: string;
|
||||||
history: string;
|
|
||||||
selectSkill: string;
|
selectSkill: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
flashMode: string;
|
flashMode: string;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ export const zhCN: Translations = {
|
||||||
sendMessagePrice:
|
sendMessagePrice:
|
||||||
"请注意,此功能将消耗token,请保证账户余额大于200可学豆。",
|
"请注意,此功能将消耗token,请保证账户余额大于200可学豆。",
|
||||||
addAttachments: "添加附件",
|
addAttachments: "添加附件",
|
||||||
history: "历史记录",
|
|
||||||
selectSkill: "选择Skill",
|
selectSkill: "选择Skill",
|
||||||
mode: "模式",
|
mode: "模式",
|
||||||
flashMode: "闪速",
|
flashMode: "闪速",
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ function asRecord(value: unknown): UnknownRecord | null {
|
||||||
return value as UnknownRecord;
|
return value as UnknownRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSelectedSkillMessage(
|
export function isSelectedSkillMessage(value: unknown): value is SelectedSkillMessage {
|
||||||
value: unknown,
|
|
||||||
): value is SelectedSkillMessage {
|
|
||||||
const record = asRecord(value);
|
const record = asRecord(value);
|
||||||
if (record?.type !== RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
if (record?.type !== RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -91,9 +89,7 @@ export function isSelectedSkillMessage(
|
||||||
return isValidId && typeof title === "string" && title.trim().length > 0;
|
return isValidId && typeof title === "string" && title.trim().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSelectedSkillsMessage(
|
export function isSelectedSkillsMessage(value: unknown): value is SelectSkillMessage {
|
||||||
value: unknown,
|
|
||||||
): value is SelectSkillMessage {
|
|
||||||
const record = asRecord(value);
|
const record = asRecord(value);
|
||||||
if (record?.type !== RECEIVE_MESSAGE_TYPES.SELECTED_SKILLS) {
|
if (record?.type !== RECEIVE_MESSAGE_TYPES.SELECTED_SKILLS) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,7 @@ export async function materializeSkillYaml(
|
||||||
): Promise<MaterializeSkillYamlResponse> {
|
): Promise<MaterializeSkillYamlResponse> {
|
||||||
console.log("[skills/api] ========== materializeSkillYaml START ==========");
|
console.log("[skills/api] ========== materializeSkillYaml START ==========");
|
||||||
console.log("[skills/api] request:", JSON.stringify(request, null, 2));
|
console.log("[skills/api] request:", JSON.stringify(request, null, 2));
|
||||||
console.log(
|
console.log("[skills/api] API URL:", `${getBackendBaseURL()}/api/skills/materialize-yaml`);
|
||||||
"[skills/api] API URL:",
|
|
||||||
`${getBackendBaseURL()}/api/skills/materialize-yaml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${getBackendBaseURL()}/api/skills/materialize-yaml`,
|
`${getBackendBaseURL()}/api/skills/materialize-yaml`,
|
||||||
|
|
@ -117,11 +114,7 @@ export async function materializeSkillYaml(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log("[skills/api] response status:", response.status, response.statusText);
|
||||||
"[skills/api] response status:",
|
|
||||||
response.status,
|
|
||||||
response.statusText,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
|
|
||||||
|
|
@ -219,10 +219,9 @@ export function useThreadStream({
|
||||||
// and to allow access to the current thread id in onUpdateEvent
|
// and to allow access to the current thread id in onUpdateEvent
|
||||||
const threadIdRef = useRef<string | null>(threadId ?? null);
|
const threadIdRef = useRef<string | null>(threadId ?? null);
|
||||||
const startedRef = useRef(false);
|
const startedRef = useRef(false);
|
||||||
const lastErrorToastRef = useRef<{
|
const lastErrorToastRef = useRef<{ message: string; timestamp: number } | null>(
|
||||||
message: string;
|
null,
|
||||||
timestamp: number;
|
);
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const listeners = useRef({
|
const listeners = useRef({
|
||||||
onStart,
|
onStart,
|
||||||
|
|
@ -455,14 +454,8 @@ export function useThreadStream({
|
||||||
try {
|
try {
|
||||||
// 新会话模式下,仅在本地已有历史消息时才重置旧线程。
|
// 新会话模式下,仅在本地已有历史消息时才重置旧线程。
|
||||||
// 对于全新 thread_id,避免多发一次 DELETE /threads/{id}(通常会 404)。
|
// 对于全新 thread_id,避免多发一次 DELETE /threads/{id}(通常会 404)。
|
||||||
if (
|
if (createNewSession && resolvedThreadId && thread.messages.length > 0) {
|
||||||
createNewSession &&
|
await apiClient.threads.delete(resolvedThreadId).catch(() => undefined);
|
||||||
resolvedThreadId &&
|
|
||||||
thread.messages.length > 0
|
|
||||||
) {
|
|
||||||
await apiClient.threads
|
|
||||||
.delete(resolvedThreadId)
|
|
||||||
.catch(() => undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload files first if any
|
// Upload files first if any
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ function normalizeThreadId(value?: string | null): string | undefined {
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function textOfMessage(message: Message) {
|
export function textOfMessage(message: Message) {
|
||||||
if (typeof message.content === "string") {
|
if (typeof message.content === "string") {
|
||||||
return message.content;
|
return message.content;
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,7 @@ export async function downloadMarkdownAsPdf(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
options: PdfOptions & {
|
options: PdfOptions & {
|
||||||
resolveAssetUrl?: (
|
resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>;
|
||||||
rawPath: string,
|
|
||||||
) => string | null | Promise<string | null>;
|
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const html2pdf = await loadHtml2Pdf();
|
const html2pdf = await loadHtml2Pdf();
|
||||||
|
|
@ -522,10 +520,7 @@ async function createImageRunFromToken(
|
||||||
const rawHref = String(token?.href ?? token?.text ?? "").trim();
|
const rawHref = String(token?.href ?? token?.text ?? "").trim();
|
||||||
if (!rawHref) return null;
|
if (!rawHref) return null;
|
||||||
|
|
||||||
const resolvedUrl = await resolveAssetReference(
|
const resolvedUrl = await resolveAssetReference(rawHref, options.resolveAssetUrl);
|
||||||
rawHref,
|
|
||||||
options.resolveAssetUrl,
|
|
||||||
);
|
|
||||||
if (!resolvedUrl || !isRenderableImageUrl(resolvedUrl)) {
|
if (!resolvedUrl || !isRenderableImageUrl(resolvedUrl)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -592,12 +587,13 @@ async function rewriteMarkdownImageSources(
|
||||||
const rawTarget = match[2]?.trim() ?? "";
|
const rawTarget = match[2]?.trim() ?? "";
|
||||||
const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
|
const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
|
||||||
if (!resolved || resolved === rawTarget) continue;
|
if (!resolved || resolved === rawTarget) continue;
|
||||||
rewritten = rewritten.replace(match[0], ``);
|
rewritten = rewritten.replace(
|
||||||
|
match[0],
|
||||||
|
``,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlMatches = [
|
const htmlMatches = [...rewritten.matchAll(/(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']+)\2/gi)];
|
||||||
...rewritten.matchAll(/(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']+)\2/gi),
|
|
||||||
];
|
|
||||||
for (const match of htmlMatches) {
|
for (const match of htmlMatches) {
|
||||||
const rawTarget = match[3]?.trim() ?? "";
|
const rawTarget = match[3]?.trim() ?? "";
|
||||||
const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
|
const resolved = await resolveAssetReference(rawTarget, resolveAssetUrl);
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ export interface UseMarkdownDownloadReturn {
|
||||||
markdown: string,
|
markdown: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
options?: PdfOptions & {
|
options?: PdfOptions & {
|
||||||
resolveAssetUrl?: (
|
resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>;
|
||||||
rawPath: string,
|
|
||||||
) => string | null | Promise<string | null>;
|
|
||||||
},
|
},
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,9 +123,7 @@ export function useMarkdownDownload(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
options?: PdfOptions & {
|
options?: PdfOptions & {
|
||||||
resolveAssetUrl?: (
|
resolveAssetUrl?: (rawPath: string) => string | null | Promise<string | null>;
|
||||||
rawPath: string,
|
|
||||||
) => string | null | Promise<string | null>;
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (isDownloading) return;
|
if (isDownloading) return;
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,7 @@ function parseStoredSkills(raw: string | null): SkillData[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSkillsByIdsFromList(
|
function removeSkillsByIdsFromList(skills: SkillData[], skillIds: string[]): SkillData[] {
|
||||||
skills: SkillData[],
|
|
||||||
skillIds: string[],
|
|
||||||
): SkillData[] {
|
|
||||||
if (skillIds.length === 0) return skills;
|
if (skillIds.length === 0) return skills;
|
||||||
const idSet = new Set(skillIds.map((id) => String(id)));
|
const idSet = new Set(skillIds.map((id) => String(id)));
|
||||||
return skills.filter((skill) => !idSet.has(String(skill.skill_id)));
|
return skills.filter((skill) => !idSet.has(String(skill.skill_id)));
|
||||||
|
|
@ -105,10 +102,7 @@ export function useIframeSkill(
|
||||||
const latestSkills = parseStoredSkills(
|
const latestSkills = parseStoredSkills(
|
||||||
window.localStorage.getItem(STORAGE_KEYS.latest),
|
window.localStorage.getItem(STORAGE_KEYS.latest),
|
||||||
);
|
);
|
||||||
const nextLatestSkills = removeSkillsByIdsFromList(
|
const nextLatestSkills = removeSkillsByIdsFromList(latestSkills, skillIds);
|
||||||
latestSkills,
|
|
||||||
skillIds,
|
|
||||||
);
|
|
||||||
if (nextLatestSkills.length > 0) {
|
if (nextLatestSkills.length > 0) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
STORAGE_KEYS.latest,
|
STORAGE_KEYS.latest,
|
||||||
|
|
@ -120,18 +114,10 @@ export function useIframeSkill(
|
||||||
|
|
||||||
const threadKey = getThreadStorageKey(threadId);
|
const threadKey = getThreadStorageKey(threadId);
|
||||||
if (threadKey) {
|
if (threadKey) {
|
||||||
const threadSkills = parseStoredSkills(
|
const threadSkills = parseStoredSkills(window.localStorage.getItem(threadKey));
|
||||||
window.localStorage.getItem(threadKey),
|
const nextThreadSkills = removeSkillsByIdsFromList(threadSkills, skillIds);
|
||||||
);
|
|
||||||
const nextThreadSkills = removeSkillsByIdsFromList(
|
|
||||||
threadSkills,
|
|
||||||
skillIds,
|
|
||||||
);
|
|
||||||
if (nextThreadSkills.length > 0) {
|
if (nextThreadSkills.length > 0) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(threadKey, JSON.stringify(nextThreadSkills));
|
||||||
threadKey,
|
|
||||||
JSON.stringify(nextThreadSkills),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem(threadKey);
|
window.localStorage.removeItem(threadKey);
|
||||||
}
|
}
|
||||||
|
|
@ -184,10 +170,7 @@ export function useIframeSkill(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
||||||
console.warn(
|
console.warn("[useIframeSkill] 忽略非法 selectedSkill 消息", event.data);
|
||||||
"[useIframeSkill] 忽略非法 selectedSkill 消息",
|
|
||||||
event.data,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
|
|
@ -203,8 +186,7 @@ export function useIframeSkill(
|
||||||
const latestSkills = parseStoredSkills(
|
const latestSkills = parseStoredSkills(
|
||||||
window.localStorage.getItem(STORAGE_KEYS.latest),
|
window.localStorage.getItem(STORAGE_KEYS.latest),
|
||||||
);
|
);
|
||||||
const restoredSkills =
|
const restoredSkills = threadSkills.length > 0 ? threadSkills : latestSkills;
|
||||||
threadSkills.length > 0 ? threadSkills : latestSkills;
|
|
||||||
if (restoredSkills.length === 0) return;
|
if (restoredSkills.length === 0) return;
|
||||||
setSelectedSkills(restoredSkills);
|
setSelectedSkills(restoredSkills);
|
||||||
setSelectedSkill(restoredSkills[0] ?? null);
|
setSelectedSkill(restoredSkills[0] ?? null);
|
||||||
|
|
@ -230,17 +212,11 @@ export function useIframeSkill(
|
||||||
}, [selectedSkills, threadId]);
|
}, [selectedSkills, threadId]);
|
||||||
|
|
||||||
// 发送选择预定义 skill
|
// 发送选择预定义 skill
|
||||||
const sendSelectSkill = useCallback(
|
const sendSelectSkill = useCallback((selectedSkills: SelectedSkillPayloadItem[]) => {
|
||||||
(selectedSkills: SelectedSkillPayloadItem[]) => {
|
const message = { type: POST_MESSAGE_TYPES.SELECT_SKILLS, selectedSkills };
|
||||||
const message = {
|
|
||||||
type: POST_MESSAGE_TYPES.SELECT_SKILLS,
|
|
||||||
selectedSkills,
|
|
||||||
};
|
|
||||||
console.log("[useIframeSkill] sendSelectSkill:", message);
|
console.log("[useIframeSkill] sendSelectSkill:", message);
|
||||||
sendToParent(message);
|
sendToParent(message);
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const bootstrapAndLockSkills = useCallback(
|
const bootstrapAndLockSkills = useCallback(
|
||||||
async ({
|
async ({
|
||||||
|
|
@ -278,9 +254,7 @@ export function useIframeSkill(
|
||||||
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
|
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
|
||||||
|
|
||||||
setIsBootstrapping(true);
|
setIsBootstrapping(true);
|
||||||
toast.loading(`正在加载技能「${title}」...`, {
|
toast.loading(`正在加载技能「${title}」...`, { id: "suggest-skill-bootstrap" });
|
||||||
id: "suggest-skill-bootstrap",
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await bootstrapRemoteSkill({
|
const result = await bootstrapRemoteSkill({
|
||||||
|
|
@ -294,9 +268,7 @@ export function useIframeSkill(
|
||||||
toast.dismiss("suggest-skill-bootstrap");
|
toast.dismiss("suggest-skill-bootstrap");
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const failedIds = selectedSkills.map((item) =>
|
const failedIds = selectedSkills.map((item) => String(item.id).trim());
|
||||||
String(item.id).trim(),
|
|
||||||
);
|
|
||||||
removeFailedSkills(failedIds);
|
removeFailedSkills(failedIds);
|
||||||
toast.error(`技能「${title}」加载失败`, {
|
toast.error(`技能「${title}」加载失败`, {
|
||||||
description: result.message || "未知错误",
|
description: result.message || "未知错误",
|
||||||
|
|
@ -313,8 +285,7 @@ export function useIframeSkill(
|
||||||
setSelectedSkills(normalizedSkills);
|
setSelectedSkills(normalizedSkills);
|
||||||
|
|
||||||
toast.success(`技能「${title}」加载成功`, {
|
toast.success(`技能「${title}」加载成功`, {
|
||||||
description:
|
description: result.message || `已创建 ${result.created_files} 个文件`,
|
||||||
result.message || `已创建 ${result.created_files} 个文件`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -322,7 +293,8 @@ export function useIframeSkill(
|
||||||
const failedIds = selectedSkills.map((item) => String(item.id).trim());
|
const failedIds = selectedSkills.map((item) => String(item.id).trim());
|
||||||
removeFailedSkills(failedIds);
|
removeFailedSkills(failedIds);
|
||||||
toast.dismiss("suggest-skill-bootstrap");
|
toast.dismiss("suggest-skill-bootstrap");
|
||||||
const message = error instanceof Error ? error.message : "网络请求失败";
|
const message =
|
||||||
|
error instanceof Error ? error.message : "网络请求失败";
|
||||||
toast.error(`技能「${title}」加载失败`, {
|
toast.error(`技能「${title}」加载失败`, {
|
||||||
description: message,
|
description: message,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -413,9 +413,8 @@
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family: "Microsoft YaHei", "微软雅黑", "PingFang SC", ui-sans-serif,
|
||||||
"Microsoft YaHei", "微软雅黑", "PingFang SC", ui-sans-serif, system-ui,
|
system-ui, sans-serif;
|
||||||
sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
|
|
@ -438,8 +437,7 @@ code,
|
||||||
kbd,
|
kbd,
|
||||||
samp,
|
samp,
|
||||||
pre {
|
pre {
|
||||||
font-family:
|
font-family: "Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
|
||||||
"Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 列表项 - 14px */
|
/* 列表项 - 14px */
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,7 @@ async function waitForArtifactsReady(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await openButton.click();
|
await openButton.click();
|
||||||
await expect(
|
await expect(page.getByTestId("artifact-file-list").first()).toBeVisible({
|
||||||
page.getByTestId("artifact-file-list").first(),
|
|
||||||
).toBeVisible({
|
|
||||||
timeout: 5_000,
|
timeout: 5_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -146,14 +144,11 @@ async function waitForArtifactsReady(
|
||||||
}
|
}
|
||||||
|
|
||||||
function artifactCardByPattern(page: Page, pattern: RegExp): Locator {
|
function artifactCardByPattern(page: Page, pattern: RegExp): Locator {
|
||||||
return page
|
return page.locator("[data-testid='artifact-file-card']").filter({
|
||||||
.locator("[data-testid='artifact-file-card']")
|
|
||||||
.filter({
|
|
||||||
has: page.locator("[data-slot='card-title'] div[title]").filter({
|
has: page.locator("[data-slot='card-title'] div[title]").filter({
|
||||||
hasText: pattern,
|
hasText: pattern,
|
||||||
}),
|
}),
|
||||||
})
|
}).first();
|
||||||
.first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
|
async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
|
||||||
|
|
@ -170,9 +165,7 @@ async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (kind === "pptx") {
|
if (kind === "pptx") {
|
||||||
await expect(
|
await expect(page.getByText("请下载ppt文件以获得最佳效果").first()).toBeVisible({
|
||||||
page.getByText("请下载ppt文件以获得最佳效果").first(),
|
|
||||||
).toBeVisible({
|
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -187,11 +180,7 @@ async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function capture(
|
async function capture(page: Page, testInfo: TestInfo, name: string): Promise<void> {
|
||||||
page: Page,
|
|
||||||
testInfo: TestInfo,
|
|
||||||
name: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const path = testInfo.outputPath(`${name}.png`);
|
const path = testInfo.outputPath(`${name}.png`);
|
||||||
await page.screenshot({ path, fullPage: true });
|
await page.screenshot({ path, fullPage: true });
|
||||||
await testInfo.attach(name, {
|
await testInfo.attach(name, {
|
||||||
|
|
@ -226,9 +215,7 @@ async function getArtifactFileNames(page: Page): Promise<string[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: if title attr is absent, use first text line of each card
|
// fallback: if title attr is absent, use first text line of each card
|
||||||
const cardTexts = await page
|
const cardTexts = await page.getByTestId("artifact-file-card").allTextContents();
|
||||||
.getByTestId("artifact-file-card")
|
|
||||||
.allTextContents();
|
|
||||||
return cardTexts
|
return cardTexts
|
||||||
.map((text) => text.split("\n")[0]?.trim() ?? "")
|
.map((text) => text.split("\n")[0]?.trim() ?? "")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,7 @@ test.describe("聊天工作台 / Artifact 面板", () => {
|
||||||
|
|
||||||
await page.getByTestId("artifacts-open-button").click();
|
await page.getByTestId("artifacts-open-button").click();
|
||||||
const imageFile = page
|
const imageFile = page
|
||||||
.locator(
|
.locator("[data-testid='artifact-file-list'] [data-testid='artifact-file-card']")
|
||||||
"[data-testid='artifact-file-list'] [data-testid='artifact-file-card']",
|
|
||||||
)
|
|
||||||
.filter({ hasText: /\.(png|jpe?g|gif|webp|svg)/i })
|
.filter({ hasText: /\.(png|jpe?g|gif|webp|svg)/i })
|
||||||
.first();
|
.first();
|
||||||
testInfo.skip(
|
testInfo.skip(
|
||||||
|
|
@ -84,9 +82,7 @@ test.describe("聊天工作台 / Artifact 面板", () => {
|
||||||
|
|
||||||
await page.getByTestId("artifacts-open-button").click();
|
await page.getByTestId("artifacts-open-button").click();
|
||||||
const htmlFile = page
|
const htmlFile = page
|
||||||
.locator(
|
.locator("[data-testid='artifact-file-list'] [data-testid='artifact-file-card']")
|
||||||
"[data-testid='artifact-file-list'] [data-testid='artifact-file-card']",
|
|
||||||
)
|
|
||||||
.filter({ hasText: /\.html?/i })
|
.filter({ hasText: /\.html?/i })
|
||||||
.first();
|
.first();
|
||||||
testInfo.skip(
|
testInfo.skip(
|
||||||
|
|
@ -106,9 +102,9 @@ test.describe("聊天工作台 / Artifact 面板", () => {
|
||||||
await htmlFile.click();
|
await htmlFile.click();
|
||||||
|
|
||||||
const htmlArtifactResponse = await htmlArtifactResponsePromise;
|
const htmlArtifactResponse = await htmlArtifactResponsePromise;
|
||||||
expect(
|
expect(htmlArtifactResponse.headers()["content-disposition"] ?? "").toContain(
|
||||||
htmlArtifactResponse.headers()["content-disposition"] ?? "",
|
"attachment;",
|
||||||
).toContain("attachment;");
|
);
|
||||||
await expect(page.getByTitle(/Artifact preview(?::.*)?$/i)).toBeVisible();
|
await expect(page.getByTitle(/Artifact preview(?::.*)?$/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -159,9 +155,10 @@ test.describe("聊天工作台 / Artifact 面板", () => {
|
||||||
await page.getByTestId("artifacts-open-button").click();
|
await page.getByTestId("artifacts-open-button").click();
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await page.getByTestId("artifact-file-card").count(), {
|
.poll(
|
||||||
timeout: 30_000,
|
async () => await page.getByTestId("artifact-file-card").count(),
|
||||||
})
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
.toBeGreaterThan(0);
|
.toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ import {
|
||||||
|
|
||||||
test.describe("聊天工作台 / 输入区与发送", () => {
|
test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
test("DF-INPUT-001 欢迎态输入框默认展开", async ({ page }, testInfo) => {
|
test("DF-INPUT-001 欢迎态输入框默认展开", async ({ page }, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const textarea = page.locator("textarea[name='message']");
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
|
@ -28,7 +32,11 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
test("DF-INPUT-002 非欢迎态输入框可展开并在失焦后收起", async ({
|
test("DF-INPUT-002 非欢迎态输入框可展开并在失焦后收起", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const textarea = page.locator("textarea[name='message']");
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
|
@ -44,17 +52,18 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
await page.locator("div.absolute.inset-0.z-1.cursor-text").click();
|
await page.locator("div.absolute.inset-0.z-1.cursor-text").click();
|
||||||
await expect.poll(inputHeight).toBeGreaterThan(120);
|
await expect.poll(inputHeight).toBeGreaterThan(120);
|
||||||
|
|
||||||
await page
|
await page.getByRole("main").first().click({ position: { x: 20, y: 20 } });
|
||||||
.getByRole("main")
|
|
||||||
.first()
|
|
||||||
.click({ position: { x: 20, y: 20 } });
|
|
||||||
await expect.poll(inputHeight).toBeLessThan(110);
|
await expect.poll(inputHeight).toBeLessThan(110);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("DF-INPUT-003 点击欢迎态建议词不会导致输入区异常", async ({
|
test("DF-INPUT-003 点击欢迎态建议词不会导致输入区异常", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const suggestions = page.getByTestId("welcome-suggestions");
|
const suggestions = page.getByTestId("welcome-suggestions");
|
||||||
|
|
@ -67,7 +76,11 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("DF-INPUT-004 空消息不可提交", async ({ page }, testInfo) => {
|
test("DF-INPUT-004 空消息不可提交", async ({ page }, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const textarea = page.locator("textarea[name='message']");
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
|
@ -82,7 +95,11 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
test("DF-INPUT-005 发送后输入框清空且只产生一条用户消息", async ({
|
test("DF-INPUT-005 发送后输入框清空且只产生一条用户消息", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const textarea = page.locator("textarea[name='message']");
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
|
@ -102,7 +119,11 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
||||||
test("DF-INPUT-006 快速重复点击发送不会重复提交", async ({
|
test("DF-INPUT-006 快速重复点击发送不会重复提交", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const textarea = page.locator("textarea[name='message']");
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ import {
|
||||||
waitForMessageListReady,
|
waitForMessageListReady,
|
||||||
} from "./support/chat-helpers";
|
} from "./support/chat-helpers";
|
||||||
|
|
||||||
async function waitForAnyMessages(
|
async function waitForAnyMessages(page: Parameters<typeof openChat>[0], timeoutMs = 15_000) {
|
||||||
page: Parameters<typeof openChat>[0],
|
|
||||||
timeoutMs = 15_000,
|
|
||||||
) {
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const count = await page.locator(".is-user, .is-assistant").count();
|
const count = await page.locator(".is-user, .is-assistant").count();
|
||||||
|
|
@ -79,10 +76,7 @@ test.describe("聊天工作台 / 消息区与历史", () => {
|
||||||
const target = element as HTMLElement;
|
const target = element as HTMLElement;
|
||||||
return target.scrollHeight - target.clientHeight > 20;
|
return target.scrollHeight - target.clientHeight > 20;
|
||||||
});
|
});
|
||||||
testInfo.skip(
|
testInfo.skip(canScroll === false, "当前线程消息区高度不足,无法触发滚动到底部按钮。");
|
||||||
canScroll === false,
|
|
||||||
"当前线程消息区高度不足,无法触发滚动到底部按钮。",
|
|
||||||
);
|
|
||||||
|
|
||||||
await messageLog.hover();
|
await messageLog.hover();
|
||||||
await page.mouse.wheel(0, -1200);
|
await page.mouse.wheel(0, -1200);
|
||||||
|
|
@ -121,10 +115,7 @@ test.describe("聊天工作台 / 消息区与历史", () => {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
expect(afterUsers.length).toBe(beforeUsers.length);
|
expect(afterUsers.length).toBe(beforeUsers.length);
|
||||||
for (const sample of beforeUsers.slice(
|
for (const sample of beforeUsers.slice(0, Math.min(3, beforeUsers.length))) {
|
||||||
0,
|
|
||||||
Math.min(3, beforeUsers.length),
|
|
||||||
)) {
|
|
||||||
expect(afterUsers.some((text) => text.includes(sample))).toBeTruthy();
|
expect(afterUsers.some((text) => text.includes(sample))).toBeTruthy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -141,10 +132,7 @@ test.describe("聊天工作台 / 消息区与历史", () => {
|
||||||
await waitForMessageListReady(page, { requireMessages: false });
|
await waitForMessageListReady(page, { requireMessages: false });
|
||||||
|
|
||||||
const todoButton = page.getByRole("button", { name: /To-dos/i });
|
const todoButton = page.getByRole("button", { name: /To-dos/i });
|
||||||
testInfo.skip(
|
testInfo.skip((await todoButton.count()) === 0, "当前线程未展示 To-dos 入口。");
|
||||||
(await todoButton.count()) === 0,
|
|
||||||
"当前线程未展示 To-dos 入口。",
|
|
||||||
);
|
|
||||||
await expect(todoButton).toBeVisible();
|
await expect(todoButton).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,9 @@ function envThread(name: string) {
|
||||||
export const PRIMARY_THREAD_ID = rawPrimaryThreadId ?? undefined;
|
export const PRIMARY_THREAD_ID = rawPrimaryThreadId ?? undefined;
|
||||||
export const THREAD_FOR_WELCOME = PRIMARY_THREAD_ID;
|
export const THREAD_FOR_WELCOME = PRIMARY_THREAD_ID;
|
||||||
export const THREAD_WITH_HISTORY = PRIMARY_THREAD_ID;
|
export const THREAD_WITH_HISTORY = PRIMARY_THREAD_ID;
|
||||||
export const THREAD_WITH_MARKDOWN = envThread(
|
export const THREAD_WITH_MARKDOWN = envThread("FRONTEND_E2E_MARKDOWN_THREAD_ID");
|
||||||
"FRONTEND_E2E_MARKDOWN_THREAD_ID",
|
|
||||||
);
|
|
||||||
export const THREAD_WITH_TODOS = envThread("FRONTEND_E2E_TODOS_THREAD_ID");
|
export const THREAD_WITH_TODOS = envThread("FRONTEND_E2E_TODOS_THREAD_ID");
|
||||||
export const THREAD_WITH_ARTIFACTS = envThread(
|
export const THREAD_WITH_ARTIFACTS = envThread("FRONTEND_E2E_ARTIFACTS_THREAD_ID");
|
||||||
"FRONTEND_E2E_ARTIFACTS_THREAD_ID",
|
|
||||||
);
|
|
||||||
export const THREAD_WITH_IMAGE_ARTIFACT = envThread(
|
export const THREAD_WITH_IMAGE_ARTIFACT = envThread(
|
||||||
"FRONTEND_E2E_IMAGE_ARTIFACT_THREAD_ID",
|
"FRONTEND_E2E_IMAGE_ARTIFACT_THREAD_ID",
|
||||||
);
|
);
|
||||||
|
|
@ -112,9 +108,10 @@ export async function waitForMessageListReady(
|
||||||
await expect(page.getByRole("main").first()).toBeVisible();
|
await expect(page.getByRole("main").first()).toBeVisible();
|
||||||
if (requireMessages) {
|
if (requireMessages) {
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await page.locator(".is-user, .is-assistant").count(), {
|
.poll(
|
||||||
timeout: 30_000,
|
async () => await page.locator(".is-user, .is-assistant").count(),
|
||||||
})
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
.toBeGreaterThan(minMessages - 1);
|
.toBeGreaterThan(minMessages - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ test.use({
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("聊天工作台 / 错误提示", () => {
|
test.describe("聊天工作台 / 错误提示", () => {
|
||||||
test("DF-ERR-001 对话流失败时显示错误 toast", async ({ page }, testInfo) => {
|
test("DF-ERR-001 对话流失败时显示错误 toast", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, PRIMARY_THREAD_ID, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(testInfo, PRIMARY_THREAD_ID, "FRONTEND_E2E_THREAD_ID");
|
||||||
|
|
||||||
await openChat(page, reuseThreadChatEntry(PRIMARY_THREAD_ID!));
|
await openChat(page, reuseThreadChatEntry(PRIMARY_THREAD_ID!));
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,7 @@ test.use({
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("线程路由(无 isnew)", () => {
|
test.describe("线程路由(无 isnew)", () => {
|
||||||
test("/new 始终走欢迎态,发送后进入具体 thread 路由", async ({
|
test("/new 始终走欢迎态,发送后进入具体 thread 路由", async ({ page }, testInfo) => {
|
||||||
page,
|
|
||||||
}, testInfo) => {
|
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
||||||
|
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
@ -35,9 +33,7 @@ test.describe("线程路由(无 isnew)", () => {
|
||||||
const messageCount = await waitForAnyMessages(page);
|
const messageCount = await waitForAnyMessages(page);
|
||||||
testInfo.skip(messageCount === 0, "当前线程没有可见历史消息。");
|
testInfo.skip(messageCount === 0, "当前线程没有可见历史消息。");
|
||||||
|
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL(new RegExp(`/workspace/chats/${THREAD_FOR_WELCOME!}`));
|
||||||
new RegExp(`/workspace/chats/${THREAD_FOR_WELCOME!}`),
|
|
||||||
);
|
|
||||||
await expect(page.locator(".is-user, .is-assistant").first()).toBeVisible();
|
await expect(page.locator(".is-user, .is-assistant").first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,13 +62,12 @@ test.describe("线程路由(无 isnew)", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendMessage(page, text);
|
await sendMessage(page, text);
|
||||||
await expect(
|
await expect(page.locator(".is-user").filter({ hasText: text })).toHaveCount(1);
|
||||||
page.locator(".is-user").filter({ hasText: text }),
|
|
||||||
).toHaveCount(1);
|
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await page.locator(".is-assistant").count(), {
|
.poll(
|
||||||
timeout: 30_000,
|
async () => await page.locator(".is-assistant").count(),
|
||||||
})
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
.toBeGreaterThan(0);
|
.toBeGreaterThan(0);
|
||||||
|
|
||||||
const streamRequest = await streamRequestPromise;
|
const streamRequest = await streamRequestPromise;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
test("DF-ROUTE-001 /new 带 thread_id 时展示欢迎态与建议词", async ({
|
test("DF-ROUTE-001 /new 带 thread_id 时展示欢迎态与建议词", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
const suggestions = page.getByTestId("welcome-suggestions");
|
const suggestions = page.getByTestId("welcome-suggestions");
|
||||||
|
|
@ -27,7 +31,11 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
test("DF-ROUTE-002 /new 复用模式保留欢迎态且不直接渲染历史", async ({
|
test("DF-ROUTE-002 /new 复用模式保留欢迎态且不直接渲染历史", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, reuseThreadWelcomeEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, reuseThreadWelcomeEntry(THREAD_FOR_WELCOME!));
|
||||||
|
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL(
|
||||||
|
|
@ -37,7 +45,9 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
await expect(page.locator(".is-user, .is-assistant")).toHaveCount(0);
|
await expect(page.locator(".is-user, .is-assistant")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("DF-ROUTE-003 /new 缺少 thread_id 时进入欢迎态", async ({ page }) => {
|
test("DF-ROUTE-003 /new 缺少 thread_id 时进入欢迎态", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
await page.goto(invalidNewChatUrl());
|
await page.goto(invalidNewChatUrl());
|
||||||
|
|
||||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||||
|
|
@ -47,7 +57,11 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
test("DF-ROUTE-004 线程页直接展示历史消息与标题栏", async ({
|
test("DF-ROUTE-004 线程页直接展示历史消息与标题栏", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||||
await waitForMessageListReady(page);
|
await waitForMessageListReady(page);
|
||||||
|
|
||||||
|
|
@ -62,7 +76,11 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
test("DF-ROUTE-005 退出确认取消后保持当前线程页面", async ({
|
test("DF-ROUTE-005 退出确认取消后保持当前线程页面", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||||
await waitForMessageListReady(page);
|
await waitForMessageListReady(page);
|
||||||
|
|
||||||
|
|
@ -78,7 +96,11 @@ test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||||
test("DF-ROUTE-006 退出确认确认后返回带 thread_id 的 /new", async ({
|
test("DF-ROUTE-006 退出确认确认后返回带 thread_id 的 /new", async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
skipIfMissingThread(
|
||||||
|
testInfo,
|
||||||
|
THREAD_FOR_WELCOME,
|
||||||
|
"FRONTEND_E2E_THREAD_ID",
|
||||||
|
);
|
||||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||||
await waitForMessageListReady(page);
|
await waitForMessageListReady(page);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue