Files
kandev/apps/web/components/task/browser-panel.tsx
T
2026-02-16 22:43:47 +00:00

124 lines
4.2 KiB
TypeScript

'use client';
import { memo, useState, useEffect, useMemo } from 'react';
import type { IDockviewPanelProps } from 'dockview-react';
import { IconRefresh, IconExternalLink } from '@tabler/icons-react';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { PanelRoot, PanelBody, PanelHeaderBar } from './panel-primitives';
import { useAppStore } from '@/components/state-provider';
import { detectPreviewUrlFromOutput } from '@/lib/preview-url-detector';
export const BrowserPanel = memo(function BrowserPanel(
props: IDockviewPanelProps<{ url?: string }>
) {
const initialUrl = props.params.url || '';
const [userUrl, setUserUrl] = useState(initialUrl);
const [urlDraft, setUrlDraft] = useState(initialUrl);
const [refreshKey, setRefreshKey] = useState(0);
const [showIframe, setShowIframe] = useState(false);
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const devProcessId = useAppStore((state) =>
activeSessionId ? state.processes.devProcessBySessionId[activeSessionId] : undefined
);
const devOutput = useAppStore((state) =>
devProcessId ? state.processes.outputsByProcessId[devProcessId] ?? '' : ''
);
// Auto-detect URL from dev server output
const detectedUrl = detectPreviewUrlFromOutput(devOutput);
// Use user-set URL if available, otherwise fall back to detected URL
const effectiveUrl = useMemo(() => userUrl || detectedUrl || '', [userUrl, detectedUrl]);
// Derive iframe visibility: show after a delay when URL is available
const showIframeDelayed = effectiveUrl && showIframe;
// Delay showing iframe after URL is set
useEffect(() => {
if (!effectiveUrl) return;
const showTimer = setTimeout(() => setShowIframe(true), 1500);
return () => {
setShowIframe(false);
clearTimeout(showTimer);
};
}, [effectiveUrl, refreshKey]);
// Sync draft with detected URL when user hasn't typed anything
const displayDraft = urlDraft || detectedUrl || '';
const handleUrlSubmit = () => {
const trimmed = urlDraft.trim();
if (trimmed) {
setUserUrl(trimmed);
}
};
const handleOpenInTab = () => {
if (effectiveUrl) {
window.open(effectiveUrl, '_blank', 'noopener,noreferrer');
}
};
return (
<PanelRoot>
<PanelHeaderBar>
<Input
value={displayDraft}
onChange={(e) => setUrlDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleUrlSubmit();
}
}}
placeholder={detectedUrl || 'http://localhost:3000'}
className="h-6 flex-1 min-w-[180px]"
/>
<Button
size="sm"
variant="outline"
onClick={handleOpenInTab}
disabled={!effectiveUrl}
className="cursor-pointer"
title="Open in browser tab"
>
<IconExternalLink className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setRefreshKey((v) => v + 1)}
disabled={!effectiveUrl}
className="cursor-pointer"
title="Refresh"
>
<IconRefresh className="h-4 w-4" />
</Button>
</PanelHeaderBar>
<PanelBody padding={false} scroll={false}>
{showIframeDelayed ? (
<iframe
key={refreshKey}
src={effectiveUrl}
title="Browser Preview"
className="h-full w-full border-0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
referrerPolicy="no-referrer"
/>
) : effectiveUrl ? (
<div className="h-full w-full flex flex-col items-center justify-center text-muted-foreground gap-2">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
<p className="text-sm">Loading preview...</p>
</div>
) : (
<div className="h-full w-full flex flex-col items-center justify-center text-muted-foreground gap-2">
<p className="text-sm">Enter a URL above or start the dev server</p>
</div>
)}
</PanelBody>
</PanelRoot>
);
});