fix(02-07): loading overlay can never wedge the Chat/AIUI UI permanently
Demo images / Build & push demo images (push) Failing after 2m12s
Demo images / Build & push demo images (push) Failing after 2m12s
Belt-and-suspenders fix on top of AIUI's own root-cause fix (the archyBridge origin bug, fixed in the AIUI repo): the archy-side loading overlay now gets pointer-events:none (it has no interactive content of its own, so it should never have blocked clicks reaching the iframe underneath) and a bounded 8s timeout that unconditionally hides it if no 'ready' message ever arrives — regardless of AIUI/backend state. The timeout only dismisses the overlay; it does not fabricate a successful connection, so the connected indicator still reflects reality. Two new tests in chatAiuiEmbed.test.ts cover the timeout firing at exactly 8s and not firing prematurely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
08ee5ed036
commit
faf4a75ddf
@@ -15,9 +15,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading indicator while iframe loads -->
|
<!-- Loading indicator while iframe loads. pointer-events:none on the
|
||||||
|
wrapper (see <style>) so this never blocks clicks reaching the
|
||||||
|
iframe underneath even while shown; the bounded timeout below
|
||||||
|
(see aiuiLoadTimedOut) additionally guarantees it disappears
|
||||||
|
outright regardless of backend/handshake state, so it can never
|
||||||
|
wedge the UI permanently. -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="aiuiUrl && !aiuiConnected" class="chat-loading" role="status" aria-live="polite">
|
<div v-if="aiuiUrl && !aiuiConnected && !aiuiLoadTimedOut" class="chat-loading" role="status" aria-live="polite">
|
||||||
<div class="glass-card p-8 flex flex-col items-center gap-4">
|
<div class="glass-card p-8 flex flex-col items-center gap-4">
|
||||||
<div class="chat-loading-spinner" aria-hidden="true" />
|
<div class="chat-loading-spinner" aria-hidden="true" />
|
||||||
<p class="text-sm text-white/60">{{ t('chat.loadingAssistant') }}</p>
|
<p class="text-sm text-white/60">{{ t('chat.loadingAssistant') }}</p>
|
||||||
@@ -69,6 +74,17 @@ const { t } = useI18n()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
|
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
|
||||||
const aiuiConnected = ref(false)
|
const aiuiConnected = ref(false)
|
||||||
|
// Belt-and-suspenders backstop (2026-07-30 live-testing follow-up): the
|
||||||
|
// loading overlay must never be able to wedge the UI permanently regardless
|
||||||
|
// of AIUI/backend state — a broken handshake, a misconfigured origin, or the
|
||||||
|
// AI provider being unreachable must not leave the user staring at a
|
||||||
|
// spinner forever with no way to interact with the panel underneath. This
|
||||||
|
// timeout dismisses the overlay unconditionally after a bounded wait even if
|
||||||
|
// 'ready' never arrives; it does not affect aiuiConnected itself (the
|
||||||
|
// connection indicator dot still reflects the real state).
|
||||||
|
const AIUI_LOAD_TIMEOUT_MS = 8000
|
||||||
|
const aiuiLoadTimedOut = ref(false)
|
||||||
|
let loadTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
let broker: ContextBroker | null = null
|
let broker: ContextBroker | null = null
|
||||||
|
|
||||||
// D-14 presentation flags (Phase 02 Plan 07, 02-AIUI-D14.md): AIUI opens
|
// D-14 presentation flags (Phase 02 Plan 07, 02-AIUI-D14.md): AIUI opens
|
||||||
@@ -111,6 +127,7 @@ function onAiuiMessage(event: MessageEvent) {
|
|||||||
// Listen for ready messages from AIUI iframe
|
// Listen for ready messages from AIUI iframe
|
||||||
if (event.data?.type === 'ready') {
|
if (event.data?.type === 'ready') {
|
||||||
aiuiConnected.value = true
|
aiuiConnected.value = true
|
||||||
|
if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +148,14 @@ function armChatLive() {
|
|||||||
broker = new ContextBroker(aiuiFrame, aiuiUrl.value)
|
broker = new ContextBroker(aiuiFrame, aiuiUrl.value)
|
||||||
broker.start()
|
broker.start()
|
||||||
}
|
}
|
||||||
|
if (loadTimeout) clearTimeout(loadTimeout)
|
||||||
|
loadTimeout = null
|
||||||
|
if (aiuiUrl.value && !aiuiConnected.value) {
|
||||||
|
loadTimeout = setTimeout(() => {
|
||||||
|
aiuiLoadTimedOut.value = true
|
||||||
|
loadTimeout = null
|
||||||
|
}, AIUI_LOAD_TIMEOUT_MS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onActivated(() => armChatLive())
|
onActivated(() => armChatLive())
|
||||||
|
|
||||||
@@ -138,6 +163,7 @@ onDeactivated(() => {
|
|||||||
window.removeEventListener('message', onAiuiMessage)
|
window.removeEventListener('message', onAiuiMessage)
|
||||||
broker?.stop()
|
broker?.stop()
|
||||||
broker = null
|
broker = null
|
||||||
|
if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
// onActivated is a no-op outside a <KeepAlive> boundary — call it directly
|
// onActivated is a no-op outside a <KeepAlive> boundary — call it directly
|
||||||
@@ -150,6 +176,7 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('message', onAiuiMessage)
|
window.removeEventListener('message', onAiuiMessage)
|
||||||
broker?.stop()
|
broker?.stop()
|
||||||
broker = null
|
broker = null
|
||||||
|
if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -161,6 +188,10 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
/* Never let the loading state itself block interaction with the iframe
|
||||||
|
underneath — it has no interactive content of its own, so there is
|
||||||
|
nothing here that needs to capture clicks. */
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-loading-spinner {
|
.chat-loading-spinner {
|
||||||
|
|||||||
@@ -142,4 +142,42 @@ describe('Chat / AIUI embed URL stability + D-14 defaults (02-07)', () => {
|
|||||||
expect(wrapper.find('.chat-loading').exists()).toBe(false)
|
expect(wrapper.find('.chat-loading').exists()).toBe(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Belt-and-suspenders backstop added after live testing found the overlay
|
||||||
|
// could wedge the UI when the 'ready' handshake never arrives (a real bug,
|
||||||
|
// separately fixed at its root cause in AIUI's archyBridge.ts) — this
|
||||||
|
// proves the archy side never depends on that fix alone.
|
||||||
|
it('dismisses the loading overlay after a bounded timeout even if no ready message ever arrives', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
try {
|
||||||
|
const { wrapper } = mountChatInKeepAlive()
|
||||||
|
expect(wrapper.find('.chat-loading').exists()).toBe(true)
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(7999)
|
||||||
|
expect(wrapper.find('.chat-loading').exists()).toBe(true)
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(1)
|
||||||
|
expect(wrapper.find('.chat-loading').exists()).toBe(false)
|
||||||
|
// The connection indicator must NOT falsely report connected — the
|
||||||
|
// timeout only dismisses the blocking overlay, it does not fabricate
|
||||||
|
// a successful handshake.
|
||||||
|
expect(wrapper.find('[title="chat.aiuiConnected"]').exists()).toBe(false)
|
||||||
|
|
||||||
|
wrapper.unmount()
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not dismiss the loading overlay before the timeout elapses', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
try {
|
||||||
|
const { wrapper } = mountChatInKeepAlive()
|
||||||
|
await vi.advanceTimersByTimeAsync(4000)
|
||||||
|
expect(wrapper.find('.chat-loading').exists()).toBe(true)
|
||||||
|
wrapper.unmount()
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user