import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ComponentType } from 'react'; import { AlertTriangle, ArrowLeft, Film, FolderClock, Loader2, RefreshCcw, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { hostApiFetch } from '@/lib/host-api'; const DEFAULT_NIANXX_PLAY_URL = 'http://127.0.0.1:3000'; type NianxxPlayRoute = '/' | '/projects' | '/planning'; type ServiceState = 'checking' | 'starting' | 'running' | 'error'; type NianxxPlayServiceStatus = { success: boolean; running: boolean; starting: boolean; managed: boolean; baseUrl: string; port: number; projectDir?: string; pid?: number; error?: string; }; const NIANXX_PLAY_NAV: Array<{ path: NianxxPlayRoute; labelKey: string; icon: ComponentType<{ className?: string }>; }> = [ { path: '/', labelKey: 'host.nav.create', icon: Film }, { path: '/projects', labelKey: 'host.nav.projects', icon: FolderClock }, { path: '/planning', labelKey: 'host.nav.planning', icon: Sparkles }, ]; function buildEmbeddedSrc(baseUrl: string, route: NianxxPlayRoute, reloadKey: number) { try { const url = new URL(route, baseUrl); url.searchParams.set('zhinianEmbed', '1'); url.searchParams.set('zhinianHostReload', String(reloadKey)); return url.toString(); } catch { const normalizedBase = baseUrl.replace(/\/$/, ''); return `${normalizedBase}${route}?zhinianEmbed=1&zhinianHostReload=${reloadKey}`; } } export function NianxxPlay() { const { t } = useTranslation('appCenter'); const navigate = useNavigate(); const [webviewKey, setWebviewKey] = useState(0); const [currentRoute, setCurrentRoute] = useState('/'); const [loadError, setLoadError] = useState(null); const configuredAppUrl = useMemo(() => ( import.meta.env.VITE_NIANXX_PLAY_URL?.trim() || '' ), []); const [serviceState, setServiceState] = useState(configuredAppUrl ? 'running' : 'checking'); const [serviceError, setServiceError] = useState(null); const [appUrl, setAppUrl] = useState(configuredAppUrl || DEFAULT_NIANXX_PLAY_URL); const initialSrc = useMemo( () => buildEmbeddedSrc(appUrl, currentRoute, webviewKey), [appUrl, currentRoute, webviewKey], ); const ensureService = useCallback(async () => { if (configuredAppUrl) { setAppUrl(configuredAppUrl); setServiceState('running'); setServiceError(null); return true; } setServiceState('checking'); setServiceError(null); try { const status = await hostApiFetch('/api/apps/nianxx-play/status'); if (status.running) { setAppUrl(status.baseUrl); setServiceState('running'); return true; } setServiceState('starting'); const started = await hostApiFetch('/api/apps/nianxx-play/start', { method: 'POST', }); setAppUrl(started.baseUrl || DEFAULT_NIANXX_PLAY_URL); if (started.running) { setServiceState('running'); setServiceError(null); return true; } setServiceState('error'); setServiceError(started.error || t('host.serviceFailed')); return false; } catch (error) { setServiceState('error'); setServiceError(error instanceof Error ? error.message : String(error)); return false; } }, [configuredAppUrl, t]); useEffect(() => { let cancelled = false; void (async () => { const ok = await ensureService(); if (cancelled || !ok) return; setWebviewKey((value) => value + 1); })(); return () => { cancelled = true; }; }, [ensureService]); const reloadApp = async () => { setLoadError(null); const ok = await ensureService(); if (ok) { setWebviewKey((value) => value + 1); } }; const openRoute = (route: NianxxPlayRoute) => { setLoadError(null); setCurrentRoute(route); setWebviewKey((value) => value + 1); }; return (

{t('host.title')}

{serviceState === 'running' && (