From 791fa75e56911b45af6d7f0a42c28a7057979a74 Mon Sep 17 00:00:00 2001 From: G-Fourteen Date: Sun, 2 Nov 2025 20:23:57 -0700 Subject: [PATCH] Fix landing readiness UI and launch navigation --- AI/app.js | 190 ++++++++++++++++++++++++-------------- landing.js | 262 +++++++++++++++++++++++++++++++++-------------------- 2 files changed, 286 insertions(+), 166 deletions(-) diff --git a/AI/app.js b/AI/app.js index bc41972..3a30ed1 100644 --- a/AI/app.js +++ b/AI/app.js @@ -23,6 +23,7 @@ function logToScreen(message) { - Hidden by default - Toggle with ` or ~ - Word wrap + scroll + capped lines +========================= */ const heroStage = document.getElementById('hero-stage'); const heroImage = document.getElementById('hero-image'); const muteIndicator = document.getElementById('mute-indicator'); @@ -30,6 +31,10 @@ const indicatorText = muteIndicator?.querySelector('.indicator-text') ?? null; const aiCircle = document.querySelector('[data-role="ai"]'); const userCircle = document.querySelector('[data-role="user"]'); const loadingIndicator = document.getElementById('loading-indicator'); +const landingSection = document.getElementById('landing'); +const appRoot = document.getElementById('app-root'); +const launchButton = document.getElementById('launch-app'); +const recheckButton = document.getElementById('recheck-dependencies'); if (heroImage) { heroImage.setAttribute('crossorigin', 'anonymous'); @@ -52,6 +57,7 @@ let currentHeroUrl = ''; let pendingHeroUrl = ''; let currentTheme = 'dark'; let recognitionRestartTimeout = null; +let appStarted = false; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const synth = window.speechSynthesis; @@ -118,6 +124,44 @@ function resolveAssetPath(relativePath) { } } +function updateDependencyUI(results, allMet, { announce = false, missing = [] } = {}) { + const summary = missing + .map((item) => item.label || item.friendlyName || item.id) + .filter(Boolean) + .join(', '); + + if (announce) { + if (allMet) { + logToScreen('All browser requirements satisfied.'); + } else if (summary) { + logToScreen(`Missing capabilities detected: ${summary}`); + } else { + logToScreen('Some browser requirements are unavailable.'); + } + } + + return { results, allMet, missing }; +} + +function evaluateDependencies({ announce = false } = {}) { + const results = dependencyChecks.map((descriptor) => { + let met = false; + try { + met = Boolean(descriptor.check()); + } catch (error) { + console.error(`Dependency check failed for ${descriptor.id}:`, error); + } + return { ...descriptor, met }; + }); + + const missing = results.filter((result) => !result.met); + const allMet = missing.length === 0; + + updateDependencyUI(results, allMet, { announce, missing }); + + return { results, missing, allMet }; +} + document.addEventListener('DOMContentLoaded', () => { evaluateDependencies(); @@ -675,14 +719,18 @@ function isLikelyUrlSegment(segment) { } function removeMarkdownLinkTargets(value) { + if (typeof value !== 'string') { + return ''; + } + return value - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, altText, url) => { - return isLikelyUrlSegment(url) ? altText : _match; - }) - .replace(/\ \[\[^\]]*\]\(([^)]+)\)/g, (_match, linkText, url) => { - return isLikelyUrlSegment(url) ? linkText : _match; - }) - .replace(/\ \[\[ (?:command|action)[^\\]*\]\([^)]*\)\]/gi, ' '); + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, altText, url) => + isLikelyUrlSegment(url) ? altText : match + ) + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => + isLikelyUrlSegment(url) ? linkText : match + ) + .replace(/\[\[(?:command|action)[^\]]*\]\([^)]*\)\]/gi, ' '); } function removeCommandArtifacts(value) { @@ -691,15 +739,15 @@ function removeCommandArtifacts(value) { } let result = value - .replace(/\ \[\[ [^\\]*\\bcommand\\b[^\\]*\]/gi, ' ') - .replace(/\([^)]*\\bcommand\\b[^)]*\)/gi, ' ') - .replace(/<[^>]*\\bcommand\\b[^>]*>/gi, ' ') - .replace(/\\bcommands?\s*[:=-]\s*[a-z0-9_\\s-]+/gi, ' ') - .replace(/\\bactions?\s*[:=-]\s*[a-z0-9_\\s-]+/gi, ' ') - .replace(/\\b(?:execute|run)\\s+command\\s*(?:[:=-]\\s*)?[a-z0-9_-]*/gi, ' ') - .replace(/\\bcommand\\s*(?:[:=-]\\s*|\\s+)(?:[a-z0-9_-]+(?:\\s+[a-z0-9_-]+)*)?/gi, ' '); + .replace(/\[\[[^\]]*\bcommand\b[^\]]*\]\s*/gi, ' ') + .replace(/\([^)]*\bcommand\b[^)]*\)/gi, ' ') + .replace(/<[^>]*\bcommand\b[^>]*>/gi, ' ') + .replace(/\bcommands?\s*[:=-]\s*[a-z0-9_\s-]+/gi, ' ') + .replace(/\bactions?\s*[:=-]\s*[a-z0-9_\s-]+/gi, ' ') + .replace(/\b(?:execute|run)\s+command\s*(?:[:=-]\s*)?[a-z0-9_-]*/gi, ' ') + .replace(/\bcommand\s*(?:[:=-]\s*|\s+)(?:[a-z0-9_-]+(?:\s+[a-z0-9_-]+)*)?/gi, ' '); - result = result.replace(/^\\s*[-*]?\\s*(?:command|action)[^\\n]*$/gim, ' '); + result = result.replace(/^\s*[-*]?\s*(?:command|action)[^\n]*$/gim, ' '); return result; } @@ -710,35 +758,35 @@ function sanitizeForSpeech(text) { } const withoutDirectives = text - .replace(/\ \[\[command:[^\\]*\]/gi, ' ') + .replace(/\[\[command:[^\]]*\]\]/gi, ' ') .replace(/\{command:[^}]*\}/gi, ' ') .replace(/]*>[^<]*<\/command>/gi, ' ') - .replace(/\\b(?:command|action)\\s*[:=]\\s*([a-z0-9_\\-]+)/gi, ' ') - .replace(/\\bcommands?\s*[:=]\\s*([a-z0-9_\\-]+)/gi, ' ') - .replace(/\\b(?:command|action)\\s*(?:->|=>|::)\\s*([a-z0-9_\\-]+)/gi, ' ') - .replace(/\\bcommand\\s*\([^)]*\)/gi, ' '); + .replace(/\b(?:command|action)\s*[:=]\s*([a-z0-9_-]+)/gi, ' ') + .replace(/\bcommands?\s*[:=]\s*([a-z0-9_-]+)/gi, ' ') + .replace(/\b(?:command|action)\s*(?:->|=>|::)\s*([a-z0-9_-]+)/gi, ' ') + .replace(/\bcommand\s*\([^)]*\)/gi, ' '); const withoutPollinations = withoutDirectives - .replace(/https?:\\/\\/\\S*images?.pollinations.ai\\S*/gi, '') - .replace(/\\b\\S*images?.pollinations.ai\\S*\\b/gi, ''); + .replace(/https?:\/\/\S*images?.pollinations.ai\S*/gi, '') + .replace(/\b\S*images?.pollinations.ai\S*\b/gi, ''); const withoutMarkdownTargets = removeMarkdownLinkTargets(withoutPollinations); const withoutCommands = removeCommandArtifacts(withoutMarkdownTargets); const withoutGenericUrls = withoutCommands - .replace(/https?:\\/\\/\\S+/gi, ' ') - .replace(/\\bwww\\.[^\\s)]+/gi, ' '); + .replace(/https?:\/\/\S+/gi, ' ') + .replace(/\bwww\.[^\s)]+/gi, ' '); const withoutSpacedUrls = withoutGenericUrls - .replace(/h\\s*t\\s*t\\s*p\\s*s?\\s*:\\s*\\/\\/\\s*[\\w\\-./?%#&=]+/gi, ' ') - .replace(/\\bhttps?\\b/gi, ' ') - .replace(/\\bwww\\b/gi, ' '); + .replace(/h\s*t\s*t\s*p\s*s?\s*:\s*\/\/[\w\-./?%#&=]+/gi, ' ') + .replace(/\bhttps?\b/gi, ' ') + .replace(/\bwww\b/gi, ' '); const withoutSpelledUrls = withoutSpacedUrls - .replace(/h\\s*t\\s*t\\s*p\\s*s?\\s*(?:[:=]|colon)\\s*\\/\\/\\s*[\\w\\-./?%#&=]+/gi, ' ') - .replace(/\\b(?:h\\s*t\\s*t\\s*p\\s*s?|h\\s*t\\s*t\\s*p)\\b/gi, ' ') - .replace(/\\bcolon\\b/gi, ' ') - .replace(/\\bslash\\b/gi, ' '); + .replace(/h\s*t\s*t\s*p\s*s?\s*(?:[:=]|colon)\s*\/\/[\w\-./?%#&=]+/gi, ' ') + .replace(/\b(?:h\s*t\s*t\s*p\s*s?|h\s*t\s*t\s*p)\b/gi, ' ') + .replace(/\bcolon\b/gi, ' ') + .replace(/\bslash\b/gi, ' '); const parts = withoutSpelledUrls.split(/(\s+)/); const sanitizedParts = parts.map((part) => { @@ -746,15 +794,15 @@ function sanitizeForSpeech(text) { return ''; } - if (/(?:https?|www|:\/\\/|\\.com|\\.net|\\.org|\\.io|\\.ai|\\.co|\\.gov|\\.edu)/i.test(part)) { + if (/(?:https?|www|:\/\/|\.com|\.net|\.org|\.io|\.ai|\.co|\.gov|\.edu)/i.test(part)) { return ''; } - if (/\\bcommand\\b/i.test(part)) { + if (/\bcommand\b/i.test(part)) { return ''; } - if (/(?:image|artwork|photo)\\s+(?:url|link)/i.test(part)) { + if (/(?:image|artwork|photo)\s+(?:url|link)/i.test(part)) { return ''; } @@ -779,23 +827,21 @@ function sanitizeForSpeech(text) { let sanitized = sanitizedParts .join('') - .replace(/\\s{2,}/g, ' ') - .replace(/\\s+([.,!?;:])/g, '$1') + .replace(/\s{2,}/g, ' ') + .replace(/\s+([.,!?;:])/g, '$1') .replace(/\(\s*\)/g, '') - .replace(/\\\[\\s*\]/g, '') + .replace(/\[\s*\]/g, '') .replace(/\{\s*\}/g, '') - .replace(/\\b(?:https?|www)\\b/gi, '') - .replace(/\\b[a-z0-9]+\\s+dot\\s+[a-z0-9]+\\b/gi, '') - .replace(/\\b(?:dot\\s+)(?:com|net|org|io|ai|co|gov|edu|xyz)\\b/gi, '') - + .replace(/\b(?:https?|www)\b/gi, '') + .replace(/\b[a-z0-9]+\s+dot\s+[a-z0-9]+\b/gi, '') + .replace(/\b(?:dot\s+)(?:com|net|org|io|ai|co|gov|edu|xyz)\b/gi, '') .replace(/<\s*>/g, '') - .replace(/\\bcommand\\b/gi, '') - .replace(/\\b(?:image|artwork|photo)\\s+(?:url|link)\\b.*$/gim, '') + .replace(/\bcommand\b/gi, '') + .replace(/\b(?:image|artwork|photo)\s+(?:url|link)\b.*$/gim, '') .trim(); return sanitized; } - function sanitizeImageUrl(rawUrl) { if (typeof rawUrl !== 'string') { return ''; @@ -989,30 +1035,28 @@ function parseAiDirectives(responseText) { const commands = []; let workingText = responseText; - const patterns = [ - /\ \[\[command:\s*([^\\]+)\]/gi, - /\{command:\s*([^}]*)\}/gi, + const directivePatterns = [ + /\[\[\s*(?:command|action)\s*:\s*([^\]]+)\]\]/gi, + /\[\s*(?:command|action)\s*:\s*([^\]]+)\]/gi, + /\{(?:command|action)\s*:\s*([^}]+)\}/gi, /]*>\s*([^<]*)<\/command>/gi, - /\\bcommand\\s*[:=]\s*([a-z0-9_\\-]+)/gi, - /\\bcommands?\s*[:=]\s*([a-z0-9_\\-]+)/gi, - /\\baction\s*[:=]\s*([a-z0-9_\\-]+)/gi, - /\\b(?:command|action)\\s*(?:->|=>|::)\\s*([a-z0-9_\\-]+)/gi, - /\\bcommand\\s*\(\s*([^)]+?)\s*\)/gi + /\b(?:command|action)\s*[:=]\s*([a-z0-9_\-]+)/gi, + /\bcommands?\s*[:=]\s*([a-z0-9_\-]+)/gi, + /\b(?:command|action)\s*(?:->|=>|::)\s*([a-z0-9_\-]+)/gi, + /\bcommand\s*\(\s*([^)]+?)\s*\)/gi ]; - for (const pattern of patterns) { - workingText = workingText.replace(pattern, (_match, commandValue) => { - if (commandValue) { - const normalized = normalizeCommandValue(commandValue); - if (normalized) { - commands.push(normalized); - } + for (const pattern of directivePatterns) { + workingText = workingText.replace(pattern, (_match, commandValue = '') => { + const normalized = normalizeCommandValue(commandValue); + if (normalized) { + commands.push(normalized); } return ' '; }); } - const slashCommandRegex = /(?:^|\s)\/ (open_image|save_image|copy_image|mute_microphone|unmute_microphone|stop_speaking|shutup|set_model_flux|set_model_turbo|set_model_kontext|clear_chat_history|theme_light|theme_dark)\\b/gi; + const slashCommandRegex = /(?:^|\s)\/\s*(open_image|save_image|copy_image|mute_microphone|unmute_microphone|stop_speaking|shutup|set_model_flux|set_model_turbo|set_model_kontext|clear_chat_history|theme_light|theme_dark)\b/gi; workingText = workingText.replace(slashCommandRegex, (_match, commandValue) => { const normalized = normalizeCommandValue(commandValue); if (normalized) { @@ -1021,10 +1065,11 @@ function parseAiDirectives(responseText) { return ' '; }); - const directiveBlockRegex = /(?:^|\\n)\\s*(?:commands?|actions?)\\s*:?\\s*(?:\\n|$ )((?:\\s*[-*•]?\\s*[a-z0-9_\\-]+\\s*(?:\\(\\))?\\s*(?:\\n|$))+)/gi; - workingText = workingText.replace(directiveBlockRegex, (_match, blockContent) => { + const directiveBlockRegex = /(?:^|\n)\s*(?:commands?|actions?)\s*:?\s*(?:\n|$)((?:\s*[-*•]?\s*[a-z0-9_\-]+(?:\s*\(\s*[a-z0-9_\-]*\s*\))?\s*(?:\n|$))+)/gi; + + workingText = workingText.replace(directiveBlockRegex, (_match, blockContent = '') => { const lines = blockContent - .split(/\\n+/) // Split by one or more newlines + .split(/\n+/) .map((line) => line.replace(/^[^a-z0-9]+/i, '').trim()) .filter(Boolean); @@ -1035,15 +1080,14 @@ function parseAiDirectives(responseText) { } } - return '\\n'; + return '\n'; }); - const cleanedText = workingText.replace(/\\n{3,}/g, '\\n\\n').trim(); + const cleanedText = workingText.replace(/\n{3,}/g, '\n\n').trim(); const uniqueCommands = [...new Set(commands)]; return { cleanedText, commands: uniqueCommands }; } - async function executeAiCommand(command, options = {}) { if (!command) { return false; @@ -1379,6 +1423,18 @@ async function getAIResponse(userInput) { } } + const responsePayload = { + text: finalAssistantMessage, + rawText: aiText, + commands, + imageUrl: selectedImageUrl || '', + fallbackImageUrl, + heroState: heroStage?.dataset.state || '', + heroUrl: getImageUrl() || pendingHeroUrl || '', + theme: document.body?.dataset.theme || currentTheme + }; + + return responsePayload; } catch (error) { console.error('Error getting text from Pollinations AI:', error); setCircleState(aiCircle, { @@ -1392,6 +1448,8 @@ async function getAIResponse(userInput) { label: 'Unity is idle' }); }, 2400); + + return { error: error instanceof Error ? error : new Error('AI response failed') }; } } diff --git a/landing.js b/landing.js index ae95012..e5c8c29 100644 --- a/landing.js +++ b/landing.js @@ -1,9 +1,14 @@ (() => { const dependencyLight = document.querySelector('[data-role="dependency-light"]'); const dependencySummary = document.getElementById('dependency-summary'); + const dependencyList = document.getElementById('dependency-list'); const launchButton = document.getElementById('launch-app'); + const recheckButton = document.getElementById('recheck-dependencies'); const statusMessage = document.getElementById('status-message'); + const speechSynthesisInstance = window.speechSynthesis; + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || null; + const LOOPBACK_HOST_PATTERN = /^(?:localhost|127(?:\.\d{1,3}){3}|::1|\[::1\])$/; const dependencyChecks = [ @@ -11,24 +16,26 @@ id: 'secure-context', label: 'Secure connection (HTTPS or localhost)', friendlyName: 'secure connection light', - check: () => - Boolean(window.isSecureContext) || LOOPBACK_HOST_PATTERN.test(window.location.hostname) + check: () => Boolean(window.isSecureContext) || LOOPBACK_HOST_PATTERN.test(window.location.hostname) }, { id: 'speech-recognition', label: 'Web Speech Recognition API', friendlyName: 'speech listening light', check: () => { - const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); - // Firefox uses Vosklet fallback - return Boolean(SpeechRecognition) || isFirefox; + const userAgent = (navigator.userAgent || '').toLowerCase(); + if (userAgent.includes('firefox')) { + // Firefox uses a fallback implementation (Vosklet) inside the application. + return true; + } + return Boolean(SpeechRecognition); } }, { id: 'speech-synthesis', label: 'Speech synthesis voices', friendlyName: 'talk-back voice light', - check: () => typeof synth !== 'undefined' && typeof synth.speak === 'function' + check: () => Boolean(speechSynthesisInstance && typeof speechSynthesisInstance.speak === 'function') }, { id: 'microphone', @@ -41,11 +48,8 @@ let landingInitialized = false; function setStatusMessage(message, tone = 'info') { - if (!statusMessage) { - return; - } - - statusMessage.textContent = message; + if (!statusMessage) return; + statusMessage.textContent = message || ''; if (message) { statusMessage.dataset.tone = tone; } else { @@ -54,7 +58,9 @@ } function formatDependencyList(items) { - const labels = items.map((item) => item.friendlyName ?? item.label ?? item.id).filter(Boolean); + const labels = (items || []) + .map((item) => item.friendlyName ?? item.label ?? item.id) + .filter(Boolean); if (labels.length === 0) return ''; if (labels.length === 1) return labels[0]; const head = labels.slice(0, -1).join(', '); @@ -68,13 +74,6 @@ return { passStatus, failStatus }; } - function setStatusMessage(message, tone = 'info') { - if (!statusMessage) return; - statusMessage.textContent = message; - if (message) statusMessage.dataset.tone = tone; - else delete statusMessage.dataset.tone; - } - function updateLaunchButtonState({ allMet, missing }) { if (!launchButton) return; launchButton.disabled = false; @@ -82,8 +81,12 @@ launchButton.dataset.state = allMet ? 'ready' : 'warn'; if (missing.length > 0) { const summary = formatDependencyList(missing); - launchButton.title = `Talk to Unity with limited support: ${summary}`; - } else launchButton.removeAttribute('title'); + launchButton.title = summary + ? `Talk to Unity with limited support: ${summary}` + : 'Talk to Unity with limited support'; + } else { + launchButton.removeAttribute('title'); + } } function showRecheckInProgress() { @@ -96,49 +99,36 @@ dependencyLight.dataset.state = 'pending'; dependencyLight.setAttribute('aria-label', 'Re-checking requirements'); } - if (dependencySummary) dependencySummary.textContent = 'Re-checking your setup…'; + if (dependencySummary) { + dependencySummary.textContent = 'Re-checking your setup…'; + } if (dependencyList) { dependencyList.querySelectorAll('.dependency-item').forEach((item) => { item.dataset.state = 'pending'; const statusElement = item.querySelector('.dependency-status'); - if (statusElement) statusElement.textContent = 'Checking…'; + if (statusElement) { + statusElement.textContent = 'Checking…'; + } }); } setStatusMessage('Running the readiness scan again…', 'info'); } - function handleLaunchButtonClick(event) { - console.log('handleLaunchButtonClick event:', event); - event.preventDefault(); // Prevent default button behavior (e.g., scrolling) - const result = evaluateDependencies({ announce: true }); - if (!result) return; - const { allMet, missing, results } = result; - window.dispatchEvent(new CustomEvent('talk-to-unity:launch', { detail: { allMet, missing, results } })); - } - - function handleRecheckClick() { - showRecheckInProgress(); - evaluateDependencies({ announce: true }); - } - - function bootstrapLandingExperience() { - if (landingInitialized) return; - landingInitialized = true; - evaluateDependencies(); - launchButton?.addEventListener('click', handleLaunchButtonClick); - recheckButton?.addEventListener('click', handleRecheckClick); - } - - document.addEventListener('DOMContentLoaded', bootstrapLandingExperience); - if (document.readyState !== 'loading') bootstrapLandingExperience(); - function ensureTrailingSlash(value) { if (typeof value !== 'string' || !value) return ''; return value.endsWith('/') ? value : `${value}/`; } function resolveAppLaunchUrl() { - // Fixed version — ensures the correct relative path works on all browsers + const explicitLaunchHref = launchButton?.getAttribute('data-launch-url') || launchButton?.getAttribute('href'); + if (explicitLaunchHref) { + try { + return new URL(explicitLaunchHref, window.location.href).toString(); + } catch (error) { + console.warn('Failed to resolve launch URL from link element. Falling back to computed base.', error); + } + } + const configuredBase = typeof window.__talkToUnityAssetBase === 'string' && window.__talkToUnityAssetBase ? window.__talkToUnityAssetBase @@ -147,26 +137,45 @@ if (!base) { try { - base = ensureTrailingSlash(new URL('.', window.location.href).toString()); - } catch { - console.warn('Unable to determine Talk to Unity base path. Falling back to relative navigation.'); + base = ensureTrailingSlash(new URL('./', window.location.href).toString()); + } catch (error) { + console.warn('Unable to determine Talk to Unity base path. Falling back to relative navigation.', error); base = ''; } } + const fallbackPath = './AI/index.html'; + try { - // ✅ Fixed: Always points to ./AI/index.html with proper slash - return new URL('./AI/index.html', base || window.location.href).toString(); + return new URL(fallbackPath, base || window.location.href).toString(); } catch (error) { console.warn('Failed to resolve Talk to Unity application URL. Using a relative fallback.', error); - return './AI/index.html'; + return fallbackPath; + } + } + + function syncLaunchButtonHref() { + if (!launchButton) return; + const resolvedHref = resolveAppLaunchUrl(); + if (!resolvedHref) return; + + try { + const currentHref = launchButton.href; + if (currentHref !== resolvedHref) { + launchButton.href = resolvedHref; + } + } catch (error) { + console.warn('Failed to sync launch button href. Using attribute fallback.', error); + launchButton.setAttribute('href', resolvedHref); } } function handleLaunchEvent(event) { const detail = event?.detail ?? {}; const { allMet = false, missing = [] } = detail; - if (typeof window !== 'undefined') window.__talkToUnityLaunchIntent = detail; + if (typeof window !== 'undefined') { + window.__talkToUnityLaunchIntent = detail; + } const summary = formatDependencyList(missing); const tone = allMet ? 'success' : 'warning'; @@ -178,10 +187,16 @@ setStatusMessage(launchMessage, tone); document.cookie = 'checks-passed=true;path=/'; - dependencyLight?.setAttribute('aria-label', allMet - ? 'All dependencies satisfied. Launching Talk to Unity' - : `Launching with limited functionality: ${summary}` - ); + if (dependencyLight) { + dependencyLight.setAttribute( + 'aria-label', + allMet + ? 'All dependencies satisfied. Launching Talk to Unity' + : summary + ? `Launching with limited functionality: ${summary}` + : 'Launching with limited functionality' + ); + } if (launchButton) { launchButton.disabled = true; @@ -194,52 +209,35 @@ if (appRoot && landingPage) { landingPage.setAttribute('hidden', 'true'); appRoot.removeAttribute('hidden'); - console.log('Transitioning to AI app view.'); + document.body?.setAttribute('data-app-state', 'experience'); } else { - const launchUrl = resolveAppLaunchUrl(); + syncLaunchButtonHref(); + const launchUrl = launchButton?.href || resolveAppLaunchUrl(); if (launchUrl) { - console.log('Launching AI app at:', launchUrl); window.location.href = launchUrl; } } } - window.addEventListener('talk-to-unity:launch', handleLaunchEvent); - window.addEventListener('focus', () => evaluateDependencies()); - - function evaluateDependencies({ announce = false } = {}) { - const results = dependencyChecks.map((descriptor) => { - let met = false; - try { - met = Boolean(descriptor.check()); - } catch (error) { - console.error(`Dependency check failed for ${descriptor.id}:`, error); - } - return { ...descriptor, met }; + function handleLaunchButtonClick(event) { + if (event) { + event.preventDefault(); + } + const result = evaluateDependencies({ announce: true }); + if (!result) return; + const { allMet, missing, results } = result; + const launchEvent = new CustomEvent('talk-to-unity:launch', { + detail: { allMet, missing, results } }); + window.dispatchEvent(launchEvent); + } - const missing = results.filter((r) => !r.met); - const allMet = missing.length === 0; - updateDependencyUI(results, allMet, { announce, missing }); - updateLaunchButtonState({ allMet, missing }); - - if (announce) { - if (allMet) setStatusMessage('All systems look good. Launching Talk to Unity…', 'success'); - else { - const summary = formatDependencyList(missing); - setStatusMessage( - summary - ? `Some browser features are unavailable: ${summary}. You can continue, but certain Unity abilities may be limited.` - : 'Some browser features are unavailable. You can continue, but certain Unity abilities may be limited.', - 'warning' - ); - } - } else if (allMet && statusMessage?.textContent) setStatusMessage(''); - - return { results, allMet, missing }; + function handleRecheckClick() { + showRecheckInProgress(); + evaluateDependencies({ announce: true }); } - function updateDependencyUI(results, allMet, { announce = false, missing = [] } = {}) { + function updateDependencyUI(results, allMet, { missing = [] } = {}) { if (dependencyList) { results.forEach((result) => { const item = dependencyList.querySelector(`[data-dependency="${result.id}"]`); @@ -253,19 +251,24 @@ }); } + const summary = formatDependencyList(missing); + if (dependencyLight) { dependencyLight.dataset.state = allMet ? 'pass' : 'fail'; dependencyLight.setAttribute( 'aria-label', - allMet ? 'All dependencies satisfied' : `Missing requirements: ${summary}` + allMet + ? 'All dependencies satisfied' + : summary + ? `Missing requirements: ${summary}` + : 'Missing requirements detected' ); } if (dependencySummary) { - if (missing.length === 0) + if (missing.length === 0) { dependencySummary.textContent = 'All the lights are green! Press "Talk to Unity" to start chatting.'; - else { - const summary = formatDependencyList(missing); + } else { dependencySummary.textContent = summary ? `Alerts: ${summary}. You can still launch, but features may be limited until these are resolved.` : 'Alerts detected. You can still launch, but features may be limited.'; @@ -273,6 +276,65 @@ } } - if (!announce && !allMet) setStatusMessage(''); + function evaluateDependencies({ announce = false } = {}) { + const results = dependencyChecks.map((descriptor) => { + let met = false; + try { + met = Boolean(descriptor.check()); + } catch (error) { + console.error(`Dependency check failed for ${descriptor.id}:`, error); + } + return { ...descriptor, met }; + }); + + const missing = results.filter((result) => !result.met); + const allMet = missing.length === 0; + updateDependencyUI(results, allMet, { missing }); + updateLaunchButtonState({ allMet, missing }); + + if (announce) { + if (allMet) { + setStatusMessage('All systems look good. Launching Talk to Unity…', 'success'); + } else { + const summary = formatDependencyList(missing); + setStatusMessage( + summary + ? `Some browser features are unavailable: ${summary}. You can continue, but certain Unity abilities may be limited.` + : 'Some browser features are unavailable. You can continue, but certain Unity abilities may be limited.', + 'warning' + ); + } + } else if (allMet && statusMessage?.textContent) { + setStatusMessage(''); + } + + return { results, allMet, missing }; + } + + function bootstrapLandingExperience() { + if (landingInitialized) return; + landingInitialized = true; + evaluateDependencies(); + launchButton?.addEventListener('click', handleLaunchButtonClick); + recheckButton?.addEventListener('click', handleRecheckClick); + syncLaunchButtonHref(); } + + document.addEventListener('DOMContentLoaded', bootstrapLandingExperience); + if (document.readyState !== 'loading') { + bootstrapLandingExperience(); + } + + window.addEventListener('talk-to-unity:launch', handleLaunchEvent); + window.addEventListener('focus', () => evaluateDependencies()); + window.addEventListener('load', () => { + evaluateDependencies(); + syncLaunchButtonHref(); + }); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + evaluateDependencies(); + syncLaunchButtonHref(); + } + }); })();