diff --git a/scripts/docs-site/assets.mjs b/scripts/docs-site/assets.mjs index 0ae66da5a..43c6a06e1 100644 --- a/scripts/docs-site/assets.mjs +++ b/scripts/docs-site/assets.mjs @@ -99,6 +99,7 @@ function addChatThinking(){const log=document.querySelector("[data-chat-log]");i function setChatClearVisible(visible){for(const selector of ["[data-chat-clear]","[data-chat-copy]"]){const btn=document.querySelector(selector);if(!btn)continue;if(visible)btn.removeAttribute("hidden");else btn.setAttribute("hidden","")}} function clearChat(){const log=document.querySelector("[data-chat-log]");if(!log)return;log.innerHTML=chatEmptyHtml;log.scrollTop=0;setChatClearVisible(false);document.querySelector("[data-chat-input]")?.focus()} function isAuthFetchError(err){const msg=String(err?.message||err||"").toLowerCase();return msg==="auth_required"||msg.includes("load failed")||msg.includes("failed to fetch")||msg.includes("networkerror")} +function chatFailureMessage(message){const msg=String(message||"").trim();const serverError=msg.toLowerCase().startsWith("docs agent returned 5");if(serverError)return"Molty is temporarily unavailable. Try again in a moment.";return msg||"Docs agent failed."} const pendingMoltyQuestionKey="openclaw-docs-pending-molty-question"; function chatSignInUrl(){const url=new URL("https://hub.openclaw.ai/docs/auth");const back=new URL(location.href);if(localStorage.getItem(pendingMoltyQuestionKey))back.searchParams.set("molty_resume","1");url.searchParams.set("return_to",back.href);return url.href} function initChat(){ @@ -124,8 +125,8 @@ const chatBaseRight=()=>window.matchMedia("(max-width:820px)").matches?14:18;let const maximizeIcon='';const collapseIcon='';const keepChatInViewport=()=>{if(!chat.classList.contains("open")){chatPanelRight=chatBaseRight();chat.style.left="";chat.style.right=chatPanelRight+"px";return}applyChatRight(chatPanelRight,chat.getBoundingClientRect().width)};const setExpanded=expanded=>{const targetWidth=chatTargetWidth(expanded);applyChatRight(chatPanelRight,targetWidth);chat.classList.toggle("expanded",expanded);if(maximize){maximize.setAttribute("aria-pressed",String(expanded));maximize.setAttribute("aria-label",expanded?"Restore docs assistant size":"Maximize docs assistant");maximize.innerHTML=expanded?collapseIcon:maximizeIcon}};window.addEventListener("resize",()=>keepChatInViewport()); let resetChatTimer=0;const resetChatLauncherPosition=()=>{chatPanelRight=chatBaseRight();chat.style.left="";chat.style.right=chatPanelRight+"px";chat.classList.remove("closing")};const setOpen=open=>{clearTimeout(resetChatTimer);chat.classList.toggle("open",open);if(open){chat.classList.remove("closing");applyChatRight(chatPanelRight,chat.getBoundingClientRect().width)}else{chat.classList.add("closing");resetChatTimer=setTimeout(resetChatLauncherPosition,220)}panel?.toggleAttribute("inert",!open);panel?.setAttribute("aria-hidden",String(!open));document.querySelector("[data-chat-toggle]")?.setAttribute("aria-expanded",String(open));if(open)ensureAuth().then(ok=>{if(ok)setTimeout(()=>input.focus(),0)})}; const chatText=()=>[...document.querySelectorAll(".docs-chat-message")].filter(item=>!item.classList.contains("thinking")).map(item=>(item.classList.contains("user")?"You: ":"OpenClaw: ")+item.innerText.trim()).filter(Boolean).join("\\n\\n"); -const chatErrorMessage=async res=>{const fallback="Docs agent returned "+res.status;try{const data=await res.clone().json();return typeof data?.error==="string"&&data.error.trim()?data.error.trim():fallback}catch{return fallback}}; -const sendChatMessage=async(message,{echoUser=true}={})=>{const api=window.OPENCLAW_DOCS_CHAT_API;if(!api||!message)return;if(!await ensureAuth())return;lastMessage=message;setRetryEnabled();if(echoUser)addChatMessage("user",message);setChatClearVisible(true);const reply=addChatThinking();if(submit)submit.disabled=true;if(retry)retry.disabled=true;try{const res=await fetch(api,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",redirect:"manual",body:JSON.stringify({message,retrieval:"auto",confidenceThreshold:.3})});if(res.type==="opaqueredirect"||res.redirected||res.status===0||res.status===401||res.status===403)throw new Error("AUTH_REQUIRED");if(!res.ok)throw new Error(await chatErrorMessage(res));if(!res.body)throw new Error("Docs agent did not stream a response");const reader=res.body.getReader();const decoder=new TextDecoder();let raw="";while(true){const {done,value}=await reader.read();if(done)break;raw+=decoder.decode(value,{stream:true});if(reply&&raw.trim()){if(reply.classList.contains("thinking"))reply.classList.remove("thinking");reply.innerHTML=renderChatText(raw);reply.parentElement.scrollTop=reply.parentElement.scrollHeight}}if(!raw&&reply){reply.classList.remove("thinking");reply.innerHTML="
No response.
"}}catch(err){if(reply){const msg=isAuthFetchError(err)?"[Verify with GitHub]("+chatSignInUrl()+"), then ask again.":err?.message||"Docs agent failed.";reply.className="docs-chat-message error";reply.innerHTML=renderChatText(msg);if(isAuthFetchError(err))setAuthState("required")}}finally{if(submit)submit.disabled=authState!=="ready";setRetryEnabled();if(authState==="ready")input.focus()}}; +const chatErrorMessage=async res=>{const fallback="Docs agent returned "+res.status;if(res.status>=500)return fallback;try{const data=await res.clone().json();return typeof data?.error==="string"&&data.error.trim()?data.error.trim():fallback}catch{return fallback}}; +const sendChatMessage=async(message,{echoUser=true}={})=>{const api=window.OPENCLAW_DOCS_CHAT_API;if(!api||!message)return;if(!await ensureAuth())return;lastMessage=message;setRetryEnabled();if(echoUser)addChatMessage("user",message);setChatClearVisible(true);const reply=addChatThinking();if(submit)submit.disabled=true;if(retry)retry.disabled=true;try{const res=await fetch(api,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",redirect:"manual",body:JSON.stringify({message,retrieval:"auto",confidenceThreshold:.3})});if(res.type==="opaqueredirect"||res.redirected||res.status===0||res.status===401||res.status===403)throw new Error("AUTH_REQUIRED");if(!res.ok)throw new Error(await chatErrorMessage(res));if(!res.body)throw new Error("Docs agent did not stream a response");const reader=res.body.getReader();const decoder=new TextDecoder();let raw="";while(true){const {done,value}=await reader.read();if(done)break;raw+=decoder.decode(value,{stream:true});if(reply&&raw.trim()){if(reply.classList.contains("thinking"))reply.classList.remove("thinking");reply.innerHTML=renderChatText(raw);reply.parentElement.scrollTop=reply.parentElement.scrollHeight}}if(!raw&&reply){reply.classList.remove("thinking");reply.innerHTML="No response.
"}}catch(err){if(reply){const msg=isAuthFetchError(err)?"[Verify with GitHub]("+chatSignInUrl()+"), then ask again.":chatFailureMessage(err?.message);reply.className="docs-chat-message error";reply.innerHTML=renderChatText(msg);if(isAuthFetchError(err))setAuthState("required")}}finally{if(submit)submit.disabled=authState!=="ready";setRetryEnabled();if(authState==="ready")input.focus()}}; const askMoltyQuestion=async message=>{const question=String(message||"").trim();setOpen(true);if(!question){localStorage.removeItem(pendingMoltyQuestionKey);await ensureAuth();return}localStorage.setItem(pendingMoltyQuestionKey,question);if(!await ensureAuth())return;localStorage.removeItem(pendingMoltyQuestionKey);await sendChatMessage(question)}; window.OPENCLAW_DOCS_ASK_MOLTY=askMoltyQuestion; const resumeMolty=new URLSearchParams(location.search).get("molty_resume")==="1";const pendingMoltyQuestion=localStorage.getItem(pendingMoltyQuestionKey);if(resumeMolty&&pendingMoltyQuestion){const clean=new URL(location.href);clean.searchParams.delete("molty_resume");history.replaceState(history.state,"",clean.href);setTimeout(()=>askMoltyQuestion(pendingMoltyQuestion),0)} diff --git a/scripts/docs-site/smoke.mjs b/scripts/docs-site/smoke.mjs index b9d1bd03f..b896d4b65 100644 --- a/scripts/docs-site/smoke.mjs +++ b/scripts/docs-site/smoke.mjs @@ -322,6 +322,10 @@ if (/data-locale/.test(siteJs)) { if (!/function initChat/.test(siteJs) || !/data-chat-form/.test(siteJs) || !/chat\.dataset\.chatAuthState=state/.test(siteJs) + || !/function chatFailureMessage/.test(siteJs) + || !/serverError=msg\.toLowerCase\(\)\.startsWith\("docs agent returned 5"\)/.test(siteJs) + || !/if\(res\.status>=500\)return fallback/.test(siteJs) + || !/Molty is temporarily unavailable\. Try again in a moment\./.test(siteJs) || !/panel\?\.toggleAttribute\("inert",!open\)/.test(siteJs) || !/panel\?\.setAttribute\("aria-hidden",String\(!open\)\)/.test(siteJs) || !/form\.inert=!ready/.test(siteJs) diff --git a/scripts/docs-site/visual-smoke.mjs b/scripts/docs-site/visual-smoke.mjs index 6f45b2091..91766e834 100644 --- a/scripts/docs-site/visual-smoke.mjs +++ b/scripts/docs-site/visual-smoke.mjs @@ -222,6 +222,10 @@ async function checkDesktop() { throw new Error(`search shortcut inline hint failed: ${JSON.stringify(searchShortcut)}`); } await page.keyboard.press(process.platform === "darwin" ? "Meta+K" : "Control+K"); + await page.waitForFunction(() => + document.querySelector(".search-modal")?.classList.contains("open") + && document.activeElement?.matches("[data-search-input]") + ); const searchOpen = await page.evaluate(() => ({ open: document.querySelector(".search-modal")?.classList.contains("open"), focused: document.activeElement?.matches("[data-search-input]"),