diff --git a/src/app/page.tsx b/src/app/page.tsx index 759e638..32c2eaf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -169,26 +169,20 @@ export function HeroSection() { const presentation = resolveHomepageVideoPresentation(entry); const heroCopy = isChinese - ? { + ? { eyebrow: "AI Native Workspace", - title: "直接说出你的需求,剩下的交给 AI", subtitle: "从想法到上线,AI 自动完成构建、部署与优化。", demoLabel: "产品演示", - demoHint: - "这里展示当前域名对应的产品演示链接。主站默认走 YouTube,中国站可切到 Bilibili,也可以继续按域名覆盖。", } : { eyebrow: "AI Native Workspace", - title: "Describe what you need. Let AI handle the rest.", subtitle: "From idea to launch, AI can assemble, deploy, and optimize the work.", demoLabel: "Product demo", - demoHint: - "This section resolves the product demo for the current host. The default can use YouTube while regional hosts override it.", }; return ( -
+
@@ -196,35 +190,25 @@ export function HeroSection() {
-
-
-
-
+
+
+
+

{heroCopy.demoLabel}

-

- {heroCopy.demoHint} -

- - {entry.domain?.trim() - ? `${isChinese ? "当前域名" : "Host"}: ${entry.domain}` - : isChinese - ? "默认主站配置" - : "Default site config"} -
-
+
{isChinese ? "打开原始链接" : "Open source link"} @@ -232,47 +216,32 @@ export function HeroSection() {
-
-
+
+

{heroCopy.eyebrow}

-

- {heroCopy.title} -

-

+

{heroCopy.subtitle}

-
-
-
+
+
+

{isChinese ? "X 助手" : "X Assistant"}

-

- {isChinese - ? "首页只保留一个主路径:先提问,再由助手拆解任务、调用能力并推进执行。" - : "The homepage keeps one primary path: ask first, then let the assistant plan and execute."} -

- + {isChinese ? "对话即入口" : "Prompt-first"}
-
+
ttlMs; + + if (parsed.scope !== scope) { + window.sessionStorage.removeItem(PAIRING_REQUIRED_SESSION_STORAGE_KEY); + return { state: null, expired: false }; + } + + if (isExpired) { + window.sessionStorage.removeItem(PAIRING_REQUIRED_SESSION_STORAGE_KEY); + return { state: null, expired: true }; + } + + return { + state: { + ...parsed, + ttlMs, + }, + expired: false, + }; + } catch { + return { state: null, expired: false }; + } +} + +function persistPairingRequiredState( + state: PersistedPairingRequiredState, +): void { + if (typeof window === "undefined") { + return; + } + + try { + window.sessionStorage.setItem( + PAIRING_REQUIRED_SESSION_STORAGE_KEY, + JSON.stringify(state), + ); + } catch { + // Ignore unavailable storage. + } +} + +function clearPersistedPairingRequiredState(): void { + if (typeof window === "undefined") { + return; + } + + try { + window.sessionStorage.removeItem(PAIRING_REQUIRED_SESSION_STORAGE_KEY); + } catch { + // Ignore unavailable storage. + } +} + function renderMarkdown(value: string): string { return DOMPurify.sanitize(marked.parse(value) as string); } @@ -321,6 +433,7 @@ export function OpenClawAssistantPane({ const [errorMessage, setErrorMessage] = useState(""); const [isSending, setIsSending] = useState(false); const [isCapturing, setIsCapturing] = useState(false); + const [guestSessionExpired, setGuestSessionExpired] = useState(false); const defaultsLoaded = useOpenClawConsoleStore( (state) => state.defaultsLoaded, @@ -360,9 +473,20 @@ export function OpenClawAssistantPane({ const setSelectedSessionKey = useOpenClawConsoleStore( (state) => state.setSelectedSessionKey, ); + const sessionUser = useUserStore((state) => state.user); const compact = variant === "sidebar"; const minimalPage = variant === "page"; + const pairingPersistenceUserId = + sessionUser?.uuid?.trim() || sessionUser?.id?.trim() || "anonymous"; + const pairingPersistenceTtlMs = sessionUser?.isGuest + ? PAIRING_REQUIRED_GUEST_TTL_MS + : PAIRING_REQUIRED_STATE_TTL_MS; + const pairingPersistenceScope = buildPairingPersistenceScope({ + openclawUrl, + openclawOrigin, + userId: pairingPersistenceUserId, + }); const locale = isChinese ? "zh-CN" : "en-US"; const compactConnected = compact && connectionState === "ready"; const showMinimalAgentSelect = @@ -445,6 +569,13 @@ export function OpenClawAssistantPane({ "当前没有可用的 OpenClaw 地址。先到融合设置填写 gateway / vault / APISIX,再回来启动 XWorkmate。", "No OpenClaw endpoint is available yet. Configure gateway, vault, and APISIX first, then return to XWorkmate.", ), + guestSessionExpired: pickCopy( + isChinese, + "演示模式已超过 1 小时。请注册或登录后继续使用助手。", + "Demo mode has exceeded 1 hour. Register or sign in to continue using the assistant.", + ), + login: pickCopy(isChinese, "登录", "Sign in"), + register: pickCopy(isChinese, "注册", "Register"), openIntegrations: pickCopy( isChinese, "打开接口集成", @@ -572,24 +703,32 @@ export function OpenClawAssistantPane({ const presentAssistantError = useCallback( (payload: AssistantApiErrorPayload, fallback: string) => { const signature = buildPairingRequiredSignature(payload); + const formattedMessage = formatAssistantApiError({ + payload, + isChinese, + fallback, + }); + if (signature) { if (lastPairingRequiredSignatureRef.current === signature) { return; } lastPairingRequiredSignatureRef.current = signature; + setGuestSessionExpired(false); + persistPairingRequiredState({ + signature, + errorMessage: formattedMessage, + scope: pairingPersistenceScope, + savedAtMs: Date.now(), + ttlMs: pairingPersistenceTtlMs, + }); } else { lastPairingRequiredSignatureRef.current = null; } - setErrorMessage( - formatAssistantApiError({ - payload, - isChinese, - fallback, - }), - ); + setErrorMessage(formattedMessage); }, - [isChinese], + [isChinese, pairingPersistenceScope, pairingPersistenceTtlMs], ); const connectGateway = useCallback( @@ -606,6 +745,7 @@ export function OpenClawAssistantPane({ if (!options?.force && lastConnectPairingSignatureRef.current) { setConnectionState("error"); + setErrorMessage((current) => current); return; } @@ -652,6 +792,7 @@ export function OpenClawAssistantPane({ const data = payload as OpenClawBootstrapResponse; lastConnectPairingSignatureRef.current = null; + clearPersistedPairingRequiredState(); setConnectionState("ready"); setAgents(data.agents); @@ -902,7 +1043,29 @@ export function OpenClawAssistantPane({ ); useEffect(() => { - if (!defaultsLoaded || bootstrappedRef.current) { + const persisted = inspectPersistedPairingRequiredState( + pairingPersistenceScope, + ); + if (persisted.state) { + lastPairingRequiredSignatureRef.current = persisted.state.signature; + lastConnectPairingSignatureRef.current = persisted.state.signature; + setGuestSessionExpired(false); + setConnectionState("error"); + setErrorMessage(persisted.state.errorMessage); + return; + } + + if (sessionUser?.isGuest && persisted.expired) { + lastPairingRequiredSignatureRef.current = null; + lastConnectPairingSignatureRef.current = "guest-session-expired"; + setGuestSessionExpired(true); + setConnectionState("error"); + setErrorMessage(copy.guestSessionExpired); + } + }, [copy.guestSessionExpired, pairingPersistenceScope, sessionUser?.isGuest]); + + useEffect(() => { + if (!defaultsLoaded || bootstrappedRef.current || guestSessionExpired) { return; } @@ -912,14 +1075,17 @@ export function OpenClawAssistantPane({ bootstrappedRef.current = true; void connectGateway(); - }, [connectGateway, defaultsLoaded, openclawUrl]); + }, [connectGateway, defaultsLoaded, guestSessionExpired, openclawUrl]); useEffect(() => { lastConnectPairingSignatureRef.current = null; + lastPairingRequiredSignatureRef.current = null; + setGuestSessionExpired(false); }, [ openclawOrigin, openclawToken, openclawUrl, + pairingPersistenceUserId, vaultNamespace, vaultSecretKey, vaultSecretPath, @@ -1124,7 +1290,12 @@ export function OpenClawAssistantPane({
) : null} -
+
{!compact && !minimalPage ? (
@@ -1153,7 +1324,12 @@ export function OpenClawAssistantPane({
) : null} -
+
{!showConversation ? (
@@ -1284,7 +1460,12 @@ export function OpenClawAssistantPane({ )}
-
+
{modeOptions.map((option) => ( + +
+ ) : null}
) : null} -
+