feat(ai-chat): implement iframe navigation and external link handling
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Added `useAiIframeNavigation` hook to manage iframe navigation and enforce base paths. - Introduced `useAiIframeExternalLinks` hook to handle external link opening in the parent tab. - Updated `AiChatIframe` component to utilize the new hooks for improved user experience.
This commit is contained in:
parent
7ee1a66942
commit
3477361db0
@ -4,6 +4,10 @@ import { useEffect, useMemo, useRef } from "react"
|
||||
import type { AiChatContext } from "@/lib/ai/chat-context"
|
||||
import { buildEmbedSearchParams } from "@/lib/ai/chat-context"
|
||||
import { buildAiEmbedUrl, resolveAiEmbedOrigin } from "@/lib/ai/embed-url"
|
||||
import {
|
||||
useAiIframeExternalLinks,
|
||||
useAiIframeNavigation,
|
||||
} from "@/lib/ai/use-ai-iframe-navigation"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
type AiChatIframeProps = {
|
||||
@ -21,6 +25,9 @@ export function AiChatIframe({ publicPath = "/ai", context, className }: AiChatI
|
||||
return buildAiEmbedUrl(publicPath, qs)
|
||||
}, [publicPath, context])
|
||||
|
||||
useAiIframeNavigation(iframeRef, publicPath)
|
||||
useAiIframeExternalLinks(embedOrigin)
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current
|
||||
if (!iframe?.contentWindow || !embedOrigin) return
|
||||
|
||||
91
lib/ai/use-ai-iframe-navigation.ts
Normal file
91
lib/ai/use-ai-iframe-navigation.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { useEffect } from "react"
|
||||
import type { RefObject } from "react"
|
||||
import { resolveAiEmbedOrigin } from "@/lib/ai/embed-url"
|
||||
|
||||
function normalizePublicPath(publicPath: string): string {
|
||||
const path = (publicPath || "/ai").replace(/\/$/, "") || "/ai"
|
||||
return path.startsWith("/") ? path : `/${path}`
|
||||
}
|
||||
|
||||
/** Ultimail suite routes — never prefix with /ai (would 404 in OpenWebUI). */
|
||||
const SUITE_ROOT_PATHS = new Set([
|
||||
"/mail",
|
||||
"/drive",
|
||||
"/chat",
|
||||
"/contacts",
|
||||
"/cloud",
|
||||
"/agenda",
|
||||
"/office",
|
||||
])
|
||||
|
||||
function isSuiteRoute(pathname: string): boolean {
|
||||
for (const root of SUITE_ROOT_PATHS) {
|
||||
if (pathname === root || pathname.startsWith(`${root}/`)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Keep UltiAI iframe on the OpenWebUI subpath (e.g. /ai) — block suite landing at /. */
|
||||
export function useAiIframeNavigation(
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>,
|
||||
publicPath: string
|
||||
) {
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current
|
||||
if (!iframe) return
|
||||
|
||||
const base = normalizePublicPath(publicPath)
|
||||
const embedOrigin = resolveAiEmbedOrigin(publicPath)
|
||||
|
||||
const enforceBasePath = () => {
|
||||
try {
|
||||
const win = iframe.contentWindow
|
||||
if (!win) return
|
||||
|
||||
const { pathname, search, hash, origin } = win.location
|
||||
if (embedOrigin && origin !== embedOrigin) return
|
||||
|
||||
const inBase =
|
||||
pathname === base || pathname.startsWith(`${base}/`)
|
||||
|
||||
if (inBase) return
|
||||
|
||||
// Suite landing or explicit suite module — stay on OpenWebUI home.
|
||||
if (pathname === "/" || pathname === "" || isSuiteRoute(pathname)) {
|
||||
win.location.replace(`${base}/${search}${hash}`)
|
||||
return
|
||||
}
|
||||
|
||||
// OpenWebUI route without /ai prefix (e.g. /notes, /workspace) — preserve path.
|
||||
win.location.replace(`${base}${pathname}${search}${hash}`)
|
||||
} catch {
|
||||
// Cross-origin — parent cannot read location.
|
||||
}
|
||||
}
|
||||
|
||||
iframe.addEventListener("load", enforceBasePath)
|
||||
const timer = window.setInterval(enforceBasePath, 400)
|
||||
|
||||
return () => {
|
||||
iframe.removeEventListener("load", enforceBasePath)
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [iframeRef, publicPath])
|
||||
}
|
||||
|
||||
/** Open external links from the embed in the parent tab, not inside the iframe. */
|
||||
export function useAiIframeExternalLinks(embedOrigin: string) {
|
||||
useEffect(() => {
|
||||
if (!embedOrigin) return
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== embedOrigin) return
|
||||
const data = event.data as { type?: string; href?: string } | null
|
||||
if (data?.type !== "ULTI_OPEN_LINK" || typeof data.href !== "string") return
|
||||
window.open(data.href, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
window.addEventListener("message", onMessage)
|
||||
return () => window.removeEventListener("message", onMessage)
|
||||
}, [embedOrigin])
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user