From a8cfe1c42e48216a0e83adbb0b6e1295b025ae8a Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Wed, 8 Apr 2026 11:44:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=E5=B0=86=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E7=8A=B6=E6=80=81=E6=A0=87=E8=AF=86=E4=BB=8E=20show?= =?UTF-8?q?=5Freuse=5Fwelcome=20=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20is?= =?UTF-8?q?=5Fchatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/workspace/chats/[thread_id]/page.tsx | 9 ++-- .../workspace/chats/use-thread-chat.ts | 12 +++--- .../workspace/iframe-test-panel.tsx | 41 ++++++++++++------- .../src/components/workspace/input-box.tsx | 28 +++++++++---- frontend/src/core/iframe-messages.ts | 14 +++---- frontend/src/hooks/use-iframe-skill.ts | 37 +++++++++-------- frontend/tests/e2e/support/chat-helpers.ts | 14 +++---- 7 files changed, 92 insertions(+), 63 deletions(-) diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 635b2166..dfc7a8e6 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -37,6 +37,7 @@ import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener"; import { cn } from "@/lib/utils"; +import { IframeTestPanel } from "@/components/workspace/iframe-test-panel"; export default function ChatPage() { const { t } = useI18n(); @@ -63,7 +64,7 @@ export default function ChatPage() { showWelcomeStyle, } = useThreadChat(); - // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/show_reuse_welcome 参数。 + // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。 const shouldRenderHistory = !showWelcomeStyle; const createNewSession = useMemo(() => isNewThread, [isNewThread]); @@ -485,8 +486,8 @@ export default function ChatPage() { } setShowExitDialog(false); sendToParent({ - type: POST_MESSAGE_TYPES.SHOW_REUSE_WELCOME, - showReuseWelcome: true, + type: POST_MESSAGE_TYPES.IS_CHATTING, + isChatting: false, }); resetNewSessionState(); // 始终复用 query 中的 thread_id。 @@ -532,7 +533,7 @@ export default function ChatPage() { {/* MARK: 开发测试:iframe 通信功能测试面板 */} - {/* {process.env.NODE_ENV !== "production" && } */} + {process.env.NODE_ENV !== "production" && } ); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index 9f6098a2..b056898f 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -10,9 +10,9 @@ export function useThreadChat() { const searchParams = useSearchParams(); const threadIdFromSearchParams = searchParams.get("thread_id")?.trim(); // showWelcomeStyle的子判断 - const showReuseWelcomeFromQuery = (() => { - const showReuseWelcome = searchParams.get("show_reuse_welcome"); - return showReuseWelcome === "true"; + const isChattingFromQuery = (() => { + const isChatting = searchParams.get("is_chatting"); + return isChatting === "true"; })(); // 兜底:当 params 还未就绪时,从 pathname 解析 thread_id。 const threadIdFromPathname = (() => { @@ -77,7 +77,7 @@ export function useThreadChat() { const [isNewThread, setIsNewThread] = useState(() => isNewRoute); const [showWelcomeStyle, setShowWelcomeStyle] = useState(() => { - return isNewRoute || showReuseWelcomeFromQuery; + return isNewRoute || !isChattingFromQuery; }); // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); @@ -94,13 +94,13 @@ export function useThreadChat() { setIsNewThread(isNewRoute); // Prefer path thread id, fall back to query thread_id when path is /new. setThreadId(threadIdFromPathOrParams); - setShowWelcomeStyle(isNewRoute || showReuseWelcomeFromQuery); + setShowWelcomeStyle(isNewRoute || !isChattingFromQuery); }, [ isNewRoute, normalizeThreadId, pathname, searchParams, - showReuseWelcomeFromQuery, + isChattingFromQuery, threadId, threadIdFromPathOrParams, ]); diff --git a/frontend/src/components/workspace/iframe-test-panel.tsx b/frontend/src/components/workspace/iframe-test-panel.tsx index a900a5e3..a1589fb0 100644 --- a/frontend/src/components/workspace/iframe-test-panel.tsx +++ b/frontend/src/components/workspace/iframe-test-panel.tsx @@ -46,7 +46,7 @@ export function IframeTestPanel() { } function handleEnterSkillMode() { - router.push(`?mode=skill&skill_id=123&title=测试技能`); + router.push(`?mode=skill`); addLog("进入 mode=skill,URL 已更新"); } @@ -56,8 +56,13 @@ export function IframeTestPanel() { } function handleSendSelectSkill() { - iframeSkill.sendSelectSkill("skill_001"); - addLog("postMessage → selectSkill (skill_id=skill_001)"); + iframeSkill.sendSelectSkill(["skill_001"]); + addLog("postMessage → selectSkill (skill_id=['skill_001'])"); + } + + function handleSendSelectSkillArray() { + iframeSkill.sendSelectSkill(["1246", "1247", "1248"]); + addLog("postMessage → selectSkill (skill_id=['1246','1247','1248'])"); } function handleOpenSkillDialog() { @@ -67,7 +72,7 @@ export function IframeTestPanel() { function handleClearSkill() { iframeSkill.clearSkill(); - addLog("clearSkill 已调用,postMessage → skill_id=0"); + addLog("clearSkill 已调用,postMessage → skill_id=[]"); } function handleTestClipboardCopy() { @@ -76,12 +81,12 @@ export function IframeTestPanel() { addLog(`copyToClipboard → "${testText.slice(0, 30)}..."`); } - function handleSendShowReuseWelcome(showReuseWelcome: boolean) { + function handleSendIsChatting(isChatting: boolean) { sendToParent({ - type: POST_MESSAGE_TYPES.SHOW_REUSE_WELCOME, - showReuseWelcome: showReuseWelcome, + type: POST_MESSAGE_TYPES.IS_CHATTING, + isChatting: isChatting, }); - addLog(`postMessage → show_reuse_welcome (${showReuseWelcome})`); + addLog(`postMessage → is_chatting (${isChatting})`); } function handlePointerDown(event: ReactPointerEvent) { @@ -231,7 +236,15 @@ export function IframeTestPanel() { variant="ghost" onClick={handleSendSelectSkill} > - sendSelectSkill (skill_001) + sendSelectSkill(单个) + + @@ -327,17 +340,17 @@ export function IframeTestPanel() { - {/* 场景 5:show_reuse_welcome */} + {/* 场景 5:is_chatting */}
- ⑤ show_reuse_welcome + ⑤ is_chatting
@@ -345,7 +358,7 @@ export function IframeTestPanel() { size="sm" className="flex-1 bg-slate-50 text-xs text-slate-700 hover:bg-slate-100" variant="ghost" - onClick={() => handleSendShowReuseWelcome(false)} + onClick={() => handleSendIsChatting(false)} > 发送 false diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 9f74c531..40bc98a0 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -213,8 +213,8 @@ export function InputBox({ setIsFocused(false); if (showWelcomeStyle) { sendToParent({ - type: POST_MESSAGE_TYPES.SHOW_REUSE_WELCOME, - showReuseWelcome: false, + type: POST_MESSAGE_TYPES.IS_CHATTING, + isChatting: true, }); } onSubmit?.(message); @@ -491,7 +491,7 @@ export function InputBox({ function SuggestionListContainer({ sendSelectSkill, }: { - sendSelectSkill: (skill_id: string) => void; + sendSelectSkill: (skill_id: string[]) => void; }) { return (
@@ -504,7 +504,7 @@ function SuggestionListContainer({ function SuggestionList({ sendSelectSkill, }: { - sendSelectSkill: (skill_id: string) => void; + sendSelectSkill: (skill_id: string[]) => void; }) { const { t } = useI18n(); const { textInput } = usePromptInputController(); @@ -517,9 +517,23 @@ function SuggestionList({ ); const handleSuggestionClick = useCallback( - (suggestion: { prompt: string; skill_id?: string }) => { - // 如果有 skill_id,发送给宿主页 - if (suggestion.skill_id) { + ( + suggestion: { + prompt: string; + skill_id?: string[]; + children?: { skill_id: string[] }[]; + }, + ) => { + // 优先从 children 中提取 skill_id 数组,发送给宿主页 + const childSkillIds = (suggestion.children ?? []) + .flatMap((item) => item.skill_id) + .map((item) => item.trim()) + .filter((id): id is string => Boolean(id)); + if (childSkillIds.length > 0) { + sendSelectSkill(childSkillIds); + return; + } + if (suggestion.skill_id && suggestion.skill_id.length > 0) { sendSelectSkill(suggestion.skill_id); return; } diff --git a/frontend/src/core/iframe-messages.ts b/frontend/src/core/iframe-messages.ts index a5cd892b..56eaf49b 100644 --- a/frontend/src/core/iframe-messages.ts +++ b/frontend/src/core/iframe-messages.ts @@ -9,8 +9,8 @@ export const POST_MESSAGE_TYPES = { // 全屏切换 FULLSCREEN: "fullscreen", - // 是否展示复用欢迎态 - SHOW_REUSE_WELCOME: "showReuseWelcome", + // 会话是否处于聊天态 + IS_CHATTING: "isChatting", // 选择预定义 skill SELECT_SKILL: "selectSkill", // 打开 skill 选择对话框 @@ -35,14 +35,14 @@ export interface FullscreenMessage { fullscreen: boolean; } -export interface ShowReuseWelcomeMessage { - type: typeof POST_MESSAGE_TYPES.SHOW_REUSE_WELCOME; - showReuseWelcome: boolean; +export interface IsChattingMessage { + type: typeof POST_MESSAGE_TYPES.IS_CHATTING; + isChatting: boolean; } export interface SelectSkillMessage { type: typeof POST_MESSAGE_TYPES.SELECT_SKILL; - skill_id: string; + skill_id: string[]; } export interface OpenSkillDialogMessage { @@ -79,7 +79,7 @@ export function isSelectedSkillMessage(value: unknown): value is SelectedSkillMe export function sendToParent( message: | FullscreenMessage - | ShowReuseWelcomeMessage + | IsChattingMessage | SelectSkillMessage | OpenSkillDialogMessage, ): void { diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index afb68636..2533eca6 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -17,7 +17,7 @@ interface SkillData { // Hook 返回类型 interface UseIframeSkillReturn { selectedSkill: SkillData | null; - sendSelectSkill: (skill_id: string) => void; + sendSelectSkill: (skill_id: string[]) => void; openSkillDialog: () => void; clearSkill: () => void; } @@ -25,29 +25,30 @@ interface UseIframeSkillReturn { export function useIframeSkill(): UseIframeSkillReturn { const router = useRouter(); const searchParams = useSearchParams(); - const skillIdFromQuery = searchParams.get("skill_id"); - const titleFromQuery = searchParams.get("title"); const threadIdFromQuery = searchParams.get("thread_id"); - const showReuseWelcomeFromQuery = searchParams.get("show_reuse_welcome"); + const isChattingFromQuery = searchParams.get("is_chatting"); const lastThreadIdRef = useRef(null); const [selectedSkill, setSelectedSkill] = useState(null); - // 1. 监听 query 参数变化 - useEffect(() => { - if (skillIdFromQuery && titleFromQuery) { - setSelectedSkill({ skill_id: skillIdFromQuery, title: titleFromQuery }); - } - }, [skillIdFromQuery, titleFromQuery]); + // 1. 监听 query 参数变化(临时禁用) + // TODO: 当前 skill 仅通过 iframe postMessage 传递,暂不从 URL 读取 skill_id/title。 + // useEffect(() => { + // const skillIdFromQuery = searchParams.get("skill_id"); + // const titleFromQuery = searchParams.get("title"); + // if (skillIdFromQuery && titleFromQuery) { + // setSelectedSkill({ skill_id: skillIdFromQuery, title: titleFromQuery }); + // } + // }, [searchParams]); - // 0. 监听 query 中 show_reuse_welcome=false 且带 thread_id 时跳转到 thread 页面 + // 0. 监听 query 中 is_chatting=true 且带 thread_id 时跳转到 thread 页面 useEffect(() => { if (!threadIdFromQuery) return; - if (showReuseWelcomeFromQuery !== "false") return; + if (isChattingFromQuery !== "true") return; if (lastThreadIdRef.current === threadIdFromQuery) return; lastThreadIdRef.current = threadIdFromQuery; router.replace(`/workspace/chats/${threadIdFromQuery}`); - }, [router, showReuseWelcomeFromQuery, threadIdFromQuery]); + }, [isChattingFromQuery, router, threadIdFromQuery]); // 2. 监听宿主页 postMessage useEffect(() => { @@ -67,7 +68,7 @@ export function useIframeSkill(): UseIframeSkillReturn { }, []); // 发送选择预定义 skill - const sendSelectSkill = useCallback((skill_id: string) => { + const sendSelectSkill = useCallback((skill_id: string[]) => { const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id }; console.log("[useIframeSkill] sendSelectSkill:", message); sendToParent(message); @@ -83,12 +84,12 @@ export function useIframeSkill(): UseIframeSkillReturn { sendToParent(message); }, []); - // 清除选中并发送 skill_id=0 给主页 + // 清除选中并发送空 skill_id 数组给主页 const clearSkill = useCallback(() => { setSelectedSkill(null); - // 发送 skill_id=0 给主页,通知取消选择 - const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" }; - console.log("[useIframeSkill] clearSkill, sending skill_id=0:", message); + // 发送空数组给主页,通知取消选择 + const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id: [] }; + console.log("[useIframeSkill] clearSkill, sending skill_id=[]:", message); sendToParent(message); }, []); diff --git a/frontend/tests/e2e/support/chat-helpers.ts b/frontend/tests/e2e/support/chat-helpers.ts index 446c8e22..a46586ad 100644 --- a/frontend/tests/e2e/support/chat-helpers.ts +++ b/frontend/tests/e2e/support/chat-helpers.ts @@ -29,11 +29,11 @@ export function skipIfMissingThread( export function buildChatUrl({ pathThreadId, - showReuseWelcome, + isChatting, threadId, }: { pathThreadId?: string; - showReuseWelcome: boolean; + isChatting: boolean; threadId: string; }) { const resolvedThreadId = threadId ?? pathThreadId; @@ -42,7 +42,7 @@ export function buildChatUrl({ } const query = new URLSearchParams(); - query.set("show_reuse_welcome", String(showReuseWelcome)); + query.set("is_chatting", String(isChatting)); if (resolvedThreadId) { query.set("thread_id", resolvedThreadId); } @@ -55,20 +55,20 @@ export function buildChatUrl({ export function invalidNewChatUrl() { const query = new URLSearchParams(); - query.set("show_reuse_welcome", "true"); + query.set("is_chatting", "false"); return `/workspace/chats/new?${query.toString()}`; } export function newChatEntry(threadId: string) { return buildChatUrl({ - showReuseWelcome: true, + isChatting: false, threadId, }); } export function reuseThreadWelcomeEntry(threadId: string) { return buildChatUrl({ - showReuseWelcome: true, + isChatting: false, threadId, }); } @@ -76,7 +76,7 @@ export function reuseThreadWelcomeEntry(threadId: string) { export function reuseThreadChatEntry(threadId: string) { return buildChatUrl({ pathThreadId: threadId, - showReuseWelcome: false, + isChatting: true, threadId, }); }