diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 1ab6d05f..5d5ec91c 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -9,7 +9,8 @@ import { detectLocaleServer } from "@/core/i18n/server"; export const metadata: Metadata = { title: "XClaw", - description: "Desscriptions of XClawDesscriptions of XClawDesscriptions of XClaw", + description: + "Desscriptions of XClawDesscriptions of XClawDesscriptions of XClaw", }; export default async function RootLayout({ diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 9a06eb4b..86f0ec1f 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -62,13 +62,8 @@ export default function ChatPage() { setFullscreen: setArtifactsFullscreen, fullscreen, } = useArtifacts(); - const { - threadId, - isNewThread, - setIsNewThread, - isMock, - showWelcomeStyle, - } = useThreadChat(); + const { threadId, isNewThread, setIsNewThread, isMock, showWelcomeStyle } = + useThreadChat(); // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。 const shouldRenderHistory = !showWelcomeStyle; @@ -96,11 +91,12 @@ export default function ChatPage() { const initializedThreadRef = useRef(null); const { showNotification } = useNotification(); - const currentSlogan = - motivationSlogans[sloganIndex % motivationSlogans.length] ?? { - text: "来,一起学习工作吧", - color: "#333333", - }; + const currentSlogan = motivationSlogans[ + sloganIndex % motivationSlogans.length + ] ?? { + text: "来,一起学习工作吧", + color: "#333333", + }; const tickerCharacterList = useMemo(() => { const seen = new Set(); const uniqueChars: string[] = []; @@ -119,9 +115,12 @@ export default function ChatPage() { useEffect(() => { if (motivationSlogans.length <= 1) return; - const timer = window.setInterval(() => { - setSloganIndex((prev) => (prev + 1) % motivationSlogans.length); - }, 10 * 60 * 1000); + const timer = window.setInterval( + () => { + setSloganIndex((prev) => (prev + 1) % motivationSlogans.length); + }, + 10 * 60 * 1000, + ); return () => window.clearInterval(timer); }, []); @@ -313,7 +312,6 @@ export default function ChatPage() { setIsNewThread, ]); - return (
@@ -499,7 +499,7 @@ export default function ChatPage() {
@@ -523,38 +523,48 @@ export default function ChatPage() {
{!(showWelcomeStyle && thread.isThreadLoading) ? ( - <> - {showWelcomeStyle && !hasSubmitted && ( - - )} -
} - disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || - isSelectedSkillBootstrapping || - isUploading || - (isNewThread && !safeThreadId)} - onContextChange={(context) => setSettings("context", context)} - onSubmit={handleSubmit} - onStop={handleStop} /> - + <> + + {showWelcomeStyle && !hasSubmitted && ( + + )} +
+ } + disabled={ + env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || + isSelectedSkillBootstrapping || + isUploading || + (isNewThread && !safeThreadId) + } + onContextChange={(context) => setSettings("context", context)} + onSubmit={handleSubmit} + onStop={handleStop} + /> + ) : ( // - '' + "" )} {/* {isSelectedSkillBootstrapping && ( @@ -606,7 +616,9 @@ export default function ChatPage() { if (threadId && threadId !== "new") { nextQuery.set("thread_id", threadId); } - router.replace(`/workspace/chats/${threadId}?is_chatting=false`); + router.replace( + `/workspace/chats/${threadId}?is_chatting=false`, + ); }} > 确定 diff --git a/frontend/src/components/ai-elements/artifact.tsx b/frontend/src/components/ai-elements/artifact.tsx index e7640d87..f3c7245c 100644 --- a/frontend/src/components/ai-elements/artifact.tsx +++ b/frontend/src/components/ai-elements/artifact.tsx @@ -140,7 +140,7 @@ export const ArtifactContent = ({ className, ...props }: ArtifactContentProps) => ( -
+
{/*
*/} {/*
*/}
diff --git a/frontend/src/components/ai-elements/prompt-input.tsx b/frontend/src/components/ai-elements/prompt-input.tsx index 53491204..51cbc6c9 100644 --- a/frontend/src/components/ai-elements/prompt-input.tsx +++ b/frontend/src/components/ai-elements/prompt-input.tsx @@ -1146,8 +1146,8 @@ export const PromptInputSubmit = ({ className={cn( "h-[40px] w-[140px] rounded-[10px] border-0 font-bold transition-all", isDisabled - ? "cursor-not-allowed !bg-gray-200 text-gray-400": - "!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]", + ? "cursor-not-allowed !bg-gray-200 text-gray-400" + : "!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]", className, )} size={size} diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx index 8c9c8351..9554bc8f 100644 --- a/frontend/src/components/ui/sonner.tsx +++ b/frontend/src/components/ui/sonner.tsx @@ -7,7 +7,7 @@ const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme(); return ( - { return undefined; }, []); - const artifactViewerSandbox = "allow-same-origin allow-scripts allow-downloads"; + const artifactViewerSandbox = + "allow-same-origin allow-scripts allow-downloads"; const { content } = useArtifactContent({ threadId, filepath: filepathFromProps, @@ -316,7 +317,10 @@ export function ArtifactFileDetail({ dirnamePosix(markdownEntryPath), artifactEntryPath, ); - refToRelativeZipPath.set(ref, relativeFromMarkdown || getFileName(artifactEntryPath)); + refToRelativeZipPath.set( + ref, + relativeFromMarkdown || getFileName(artifactEntryPath), + ); if (addedVirtualPaths.has(virtualPath)) continue; addedVirtualPaths.add(virtualPath); @@ -684,7 +688,6 @@ export function ArtifactFileDetail({ {previewable && viewMode === "preview" && (language === "markdown" || language === "html") && ( - - )} {isCodeFile && viewMode === "code" && ( -
+
)} - {!isCodeFile && ( - artifactPreviewKind === "pdf" ? ( + {!isCodeFile && + (artifactPreviewKind === "pdf" ? ( ) : isOfficePreviewKind(artifactPreviewKind) ? ( - ) - )} + ))} ); @@ -820,7 +821,8 @@ function resolveReferencedVirtualPath( function collectMarkdownAssetTargets(markdown: string): Set { const targets = new Set(); const markdownRefRegex = /!?\[[^\]]*\]\(([^)]+)\)/g; - const htmlAttrRegex = /<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*(["'])([^"']+)\1/gi; + const htmlAttrRegex = + /<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*(["'])([^"']+)\1/gi; for (const match of markdown.matchAll(markdownRefRegex)) { const raw = match[1]?.trim(); @@ -878,7 +880,7 @@ export function ArtifactFilePreview({ if (language === "markdown") { return (
- ); } if (language === "html") { @@ -902,7 +903,6 @@ export function ArtifactFilePreview({ sandbox="allow-scripts allow-forms" style={{ zoom: zoomScale }} /> - ); } return null; diff --git a/frontend/src/components/workspace/chats/chat-box.tsx b/frontend/src/components/workspace/chats/chat-box.tsx index d025548b..f01deb03 100644 --- a/frontend/src/components/workspace/chats/chat-box.tsx +++ b/frontend/src/components/workspace/chats/chat-box.tsx @@ -26,10 +26,7 @@ const OPEN_MODE = { chat: 60, artifacts: 40 }; const ChatBox: React.FC<{ children: React.ReactNode; threadId: string | undefined; -}> = ({ - children, - threadId, -}) => { +}> = ({ children, threadId }) => { const { thread } = useThread(); const pathname = usePathname(); const threadIdRef = useRef(threadId); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index 209f7113..e58cbc7d 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -3,7 +3,6 @@ import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; - export function useThreadChat() { const pathname = usePathname(); const params = useParams<{ thread_id: string }>(); @@ -45,7 +44,6 @@ export function useThreadChat() { return threadIdFromPathOrParams ?? ""; }); - useEffect(() => { // 记住最近一次有效的 thread_id,供下次加载兜底使用。 if (threadId && threadId !== "new" && typeof window !== "undefined") { diff --git a/frontend/src/components/workspace/iframe-test-panel.tsx b/frontend/src/components/workspace/iframe-test-panel.tsx index 5491d96a..b461a9df 100644 --- a/frontend/src/components/workspace/iframe-test-panel.tsx +++ b/frontend/src/components/workspace/iframe-test-panel.tsx @@ -58,7 +58,9 @@ export function IframeTestPanel() { function handleSendSelectSkill() { iframeSkill.sendSelectSkill([{ id: "skill_001", name: "测试技能1" }]); - addLog("postMessage → selectedSkills ([{id:'skill_001',name:'测试技能1'}])"); + addLog( + "postMessage → selectedSkills ([{id:'skill_001',name:'测试技能1'}])", + ); } function handleSendSelectSkillArray() { @@ -187,258 +189,264 @@ export function IframeTestPanel() {
- {!collapsed &&
- {/* 当前状态 */} -
-
当前状态
-
- - mode: + {!collapsed && ( +
+ {/* 当前状态 */} +
+
当前状态
+
+ + mode: + + {isSkillMode ? "skill ✅" : "普通"} + + + + selectedSkill: + + {iframeSkill.selectedSkill + ? `${iframeSkill.selectedSkill.skill_id} / ${iframeSkill.selectedSkill.title}` + : "无"} + + +
+
+ + {/* 场景 1:侧边栏隐藏 */} +
+
+ ① 侧边栏隐藏(layout) +
+
+ + +
+
+ + {/* 场景 2:skill 选择通信 */} +
+
+ ② postMessage 通信(发送到宿主) +
+
+ + + + +
+
+ + {/* 场景 3:接收宿主页 selectedSkill */} +
+
+ ③ 接收宿主页 selectedSkill +
+
+ + + + +
+
+ + {/* 场景 4:剪贴板复制(iframe 通信) */} +
+
+ + ④ 剪贴板复制(iframe 通信) + - {isSkillMode ? "skill ✅" : "普通"} + {isInIframe ? "iframe 模式" : "独立页面"} - - - selectedSkill: - - {iframeSkill.selectedSkill - ? `${iframeSkill.selectedSkill.skill_id} / ${iframeSkill.selectedSkill.title}` - : "无"} - - -
-
- - {/* 场景 1:侧边栏隐藏 */} -
-
- ① 侧边栏隐藏(layout) -
-
- - -
-
- - {/* 场景 2:skill 选择通信 */} -
-
- ② postMessage 通信(发送到宿主) -
-
- - - - -
-
- - {/* 场景 3:接收宿主页 selectedSkill */} -
-
- ③ 接收宿主页 selectedSkill -
-
- - - - -
-
- - {/* 场景 4:剪贴板复制(iframe 通信) */} -
-
- - ④ 剪贴板复制(iframe 通信) - - - {isInIframe ? "iframe 模式" : "独立页面"} - -
-
- -
- {isInIframe - ? "将通过 postMessage 请求父页面复制" - : "将直接调用 navigator.clipboard"}
-
-
- - {/* 场景 5:is_chatting */} -
-
- ⑤ is_chatting -
-
- - -
-
- - {/* 日志 */} - {log.length > 0 && ( -
-
- 操作日志 -
- {log.map((l, i) => ( -
+ +
+ {isInIframe + ? "将通过 postMessage 请求父页面复制" + : "将直接调用 navigator.clipboard"}
- ))} +
- )} -
} + + {/* 场景 5:is_chatting */} +
+
+ ⑤ is_chatting +
+
+ + +
+
+ + {/* 日志 */} + {log.length > 0 && ( +
+
+ 操作日志 +
+ {log.map((l, i) => ( +
+ {l} +
+ ))} +
+ )} +
+ )}
); } diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index a154cd27..641e5628 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -2,8 +2,6 @@ import { useRouter } from "next/navigation"; - - import type { ChatStatus } from "ai"; import { CheckIcon, @@ -62,9 +60,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Tag } from "@/components/ui/tag"; 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 { useModels } from "@/core/models/hooks"; import type { AgentThreadContext } from "@/core/threads"; @@ -367,7 +363,7 @@ export function InputBox({ className={cn( "flex transition-all duration-300 ease-out", !effectiveIsFocused && - "pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0", + "pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0", )} > @@ -380,7 +376,7 @@ export function InputBox({ /> */} - - {showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill" && ( - - )} + {showWelcomeStyle && + !hasSubmitted && + searchParams.get("mode") !== "skill" && ( + + )} {!disabled && !showWelcomeStyle && @@ -543,19 +541,19 @@ function SuggestionList({ const promptSuggestions = suggestions.filter( ( suggestion, - ): suggestion is Exclude<(typeof suggestions)[number], { type: "separator" }> => - !("type" in suggestion), + ): suggestion is Exclude< + (typeof suggestions)[number], + { type: "separator" } + > => !("type" in suggestion), ); const handleSuggestionClick = useCallback( - ( - suggestion: { - prompt: string; - skill_id?: string[]; - children?: SelectedSkillPayloadItem[]; - suggestion: string; - }, - ) => { + (suggestion: { + prompt: string; + skill_id?: string[]; + children?: SelectedSkillPayloadItem[]; + suggestion: string; + }) => { if (isBootstrapping) return; // 优先使用 children 中的 skill(保留每个 skill 自己的 name,用于 tag 展示) @@ -564,8 +562,9 @@ function SuggestionList({ id: String(item.id).trim(), name: item.name?.trim() ?? "", })) - .filter((item): item is { id: string; name: string } => - Boolean(item.id) && Boolean(item.name), + .filter( + (item): item is { id: string; name: string } => + Boolean(item.id) && Boolean(item.name), ); if (childSkills.length > 0) { void bootstrapAndLockSkills({ @@ -604,7 +603,10 @@ function SuggestionList({ [bootstrapAndLockSkills, isBootstrapping, textInput], ); return ( - + {promptSuggestions.map((suggestion) => ( router.replace(`/workspace/chats/${threadId}?is_chatting=true`)}> - + onClick={() => + router.replace(`/workspace/chats/${threadId}?is_chatting=true`) + } + > + + + ); @@ -705,14 +726,17 @@ function IframeSkillDialogButton({ ) : null} {!isBootstrapping && selectedSkills.length > 0 ? (
{ if (event.deltaY === 0) return; event.currentTarget.scrollLeft += event.deltaY; }} > {selectedSkills.map((skill, index) => ( - + {skill.title} {/* TODO: 因为后端接口不支持取消选择skill,所以暂时禁用取消选择按钮 */}