"use client"; import { useEffect, type RefObject } from "react"; import { BrowserDetector } from "@/utils/browser-detect"; import { PlatformDetector } from "@/utils/platform-detect"; const CHAT_KEYBOARD_DEBUG_PARAM = "keyboard_debug"; interface VirtualKeyboardLike extends EventTarget { readonly overlaysContent?: boolean; readonly boundingRect?: Pick< DOMRectReadOnly, "height" | "width" | "x" | "y" >; } interface NavigatorWithVirtualKeyboard extends Navigator { readonly virtualKeyboard?: VirtualKeyboardLike; } export interface ChatKeyboardDebugSnapshot { event: string; time: number; windowInnerHeight: number; documentClientHeight: number; scrollY: number; visualViewportSupported: boolean; visualHeight: number | null; visualWidth: number | null; visualOffsetTop: number | null; visualOffsetLeft: number | null; visualScale: number | null; virtualKeyboardSupported: boolean; virtualKeyboardOverlaysContent: boolean | null; virtualKeyboardRect: { x: number; y: number; width: number; height: number; } | null; activeElement: string | null; activeBottom: number | null; activeFocused: boolean; inputBarTop: number | null; inputBarBottom: number | null; appViewportHeight: string | null; appVisibleHeight: string | null; appVisibleOffsetTop: string | null; viewportMeta: string | null; platform: { name: string; osName: string; osVersion: string; deviceVendor: string; deviceModel: string; }; browser: { name: string; version: string; inAppBrowserName: string | null; }; } declare global { interface Window { __keyboardDebugCleanup?: () => void; __keyboardDebugSnapshot?: () => ChatKeyboardDebugSnapshot; } } export interface UseChatKeyboardDiagnosticsOptions { containerRef: RefObject; } export function useChatKeyboardDiagnostics({ containerRef, }: UseChatKeyboardDiagnosticsOptions): void { useEffect(() => { if (!isChatKeyboardDiagnosticsEnabled(window.location.search)) return; return installChatKeyboardDiagnostics(containerRef); }, [containerRef]); } export function isChatKeyboardDiagnosticsEnabled(search: string): boolean { return new URLSearchParams(search).get(CHAT_KEYBOARD_DEBUG_PARAM) === "1"; } export function installChatKeyboardDiagnostics( containerRef: RefObject, ): () => void { window.__keyboardDebugCleanup?.(); const removers: Array<() => void> = []; let disposed = false; const takeSnapshot = (event: string): ChatKeyboardDebugSnapshot => { const snapshot = createChatKeyboardDebugSnapshot(event, containerRef); window.console.info("[keyboard-debug]", snapshot); return snapshot; }; const listen = ( target: EventTarget | null | undefined, eventName: string, label = eventName, ) => { if (!target) return; const handler = () => takeSnapshot(label); target.addEventListener(eventName, handler); removers.push(() => target.removeEventListener(eventName, handler)); }; const cleanup = () => { if (disposed) return; disposed = true; removers.forEach((remove) => remove()); if (window.__keyboardDebugSnapshot === manualSnapshot) { delete window.__keyboardDebugSnapshot; } if (window.__keyboardDebugCleanup === cleanup) { delete window.__keyboardDebugCleanup; } window.console.info("[keyboard-debug] removed"); }; const manualSnapshot = () => takeSnapshot("manual"); const virtualKeyboard = getVirtualKeyboard(); listen(window, "resize", "window.resize"); listen(window, "scroll", "window.scroll"); listen(window, "orientationchange", "orientationchange"); listen(document, "focusin", "focusin"); listen(document, "focusout", "focusout"); listen(document, "visibilitychange", "visibilitychange"); listen(window.visualViewport, "resize", "visualViewport.resize"); listen(window.visualViewport, "scroll", "visualViewport.scroll"); listen(virtualKeyboard, "geometrychange", "virtualKeyboard.geometrychange"); window.__keyboardDebugSnapshot = manualSnapshot; window.__keyboardDebugCleanup = cleanup; takeSnapshot("installed"); return cleanup; } function createChatKeyboardDebugSnapshot( event: string, containerRef: RefObject, ): ChatKeyboardDebugSnapshot { const visualViewport = window.visualViewport; const virtualKeyboardRect = getVirtualKeyboard()?.boundingRect ?? null; const activeElement = document.activeElement; const activeRect = activeElement instanceof HTMLElement ? activeElement.getBoundingClientRect() : null; const platform = PlatformDetector.getPlatformInfo(); const browser = BrowserDetector.getBrowserInfo(); const rootStyle = document.documentElement.style; const inputBarRect = containerRef.current?.getBoundingClientRect() ?? null; return { event, time: Math.round(performance.now()), windowInnerHeight: window.innerHeight, documentClientHeight: document.documentElement.clientHeight, scrollY: window.scrollY, visualViewportSupported: visualViewport != null, visualHeight: visualViewport?.height ?? null, visualWidth: visualViewport?.width ?? null, visualOffsetTop: visualViewport?.offsetTop ?? null, visualOffsetLeft: visualViewport?.offsetLeft ?? null, visualScale: visualViewport?.scale ?? null, virtualKeyboardSupported: getVirtualKeyboard() != null, virtualKeyboardOverlaysContent: getVirtualKeyboard()?.overlaysContent ?? null, virtualKeyboardRect: virtualKeyboardRect ? { x: virtualKeyboardRect.x, y: virtualKeyboardRect.y, width: virtualKeyboardRect.width, height: virtualKeyboardRect.height, } : null, activeElement: activeElement?.tagName ?? null, activeBottom: activeRect?.bottom ?? null, activeFocused: activeElement !== document.body, inputBarTop: inputBarRect?.top ?? null, inputBarBottom: inputBarRect?.bottom ?? null, appViewportHeight: rootStyle.getPropertyValue("--app-viewport-height") || null, appVisibleHeight: rootStyle.getPropertyValue("--app-visible-height") || null, appVisibleOffsetTop: rootStyle.getPropertyValue("--app-visible-offset-top") || null, viewportMeta: document.querySelector('meta[name="viewport"]') ?.content ?? null, platform: { name: platform.platform, osName: platform.osName, osVersion: platform.osVersion, deviceVendor: platform.deviceVendor, deviceModel: platform.deviceModel, }, browser: { name: browser.name, version: browser.version, inAppBrowserName: browser.inAppBrowserName, }, }; } function getVirtualKeyboard(): VirtualKeyboardLike | null { return ( (navigator as NavigatorWithVirtualKeyboard).virtualKeyboard ?? null ); }