Discord
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Globe, RefreshCw, Send, Terminal, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
|
||||
export const TabPreview = () => {
|
||||
const client = useClient();
|
||||
const [selectedId] = useGlobal<string | null>('BROWSER_SELECTED_TAB', null);
|
||||
const [navUrl, setNavUrl] = useState('');
|
||||
const [evalExpr, setEvalExpr] = useState('');
|
||||
const [evalResult, setEvalResult] = useState<string | null>(null);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
const [isEvaluating, setIsEvaluating] = useState(false);
|
||||
|
||||
const {
|
||||
data: screenshot,
|
||||
isLoading,
|
||||
refetch,
|
||||
isFetching,
|
||||
} = useQuery({
|
||||
queryKey: ['browser-screenshot', selectedId],
|
||||
queryFn: () => client.get<{ data: string; format: string }>(`/browser/targets/${selectedId}/screenshot`),
|
||||
enabled: !!selectedId,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const handleNavigate = useCallback(async () => {
|
||||
if (!selectedId || !navUrl.trim()) return;
|
||||
setIsNavigating(true);
|
||||
try {
|
||||
await client.post(`/browser/targets/${selectedId}/navigate`, { url: navUrl.trim() });
|
||||
setTimeout(() => void refetch(), 1000);
|
||||
} catch {
|
||||
toast.error('Navigation failed');
|
||||
} finally {
|
||||
setIsNavigating(false);
|
||||
}
|
||||
}, [client, selectedId, navUrl, refetch]);
|
||||
|
||||
const handleEvaluate = useCallback(async () => {
|
||||
if (!selectedId || !evalExpr.trim()) return;
|
||||
setIsEvaluating(true);
|
||||
try {
|
||||
const res = await client.post<{ result: unknown }>(`/browser/targets/${selectedId}/evaluate`, {
|
||||
expression: evalExpr.trim(),
|
||||
});
|
||||
setEvalResult(JSON.stringify(res.result, null, 2));
|
||||
} catch (err) {
|
||||
setEvalResult(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setIsEvaluating(false);
|
||||
}
|
||||
}, [client, selectedId, evalExpr]);
|
||||
|
||||
if (!selectedId) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm opacity-50">
|
||||
<Globe className="h-8 w-8" />
|
||||
<span>Select a tab to preview</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* URL bar */}
|
||||
<div className="flex items-center gap-2 border-b px-3 py-2">
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0" onClick={() => void refetch()} disabled={isFetching}>
|
||||
{isFetching ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<form
|
||||
className="flex flex-1 items-center gap-2"
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
void handleNavigate();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={navUrl}
|
||||
onChange={(ev) => setNavUrl(ev.target.value)}
|
||||
placeholder="Navigate to URL..."
|
||||
className="flex-1 rounded-md border bg-transparent px-2 py-1 text-sm outline-none focus:border-cyan-500"
|
||||
/>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0" type="submit" disabled={isNavigating || !navUrl.trim()}>
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Screenshot */}
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{isLoading && !screenshot && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin opacity-50" />
|
||||
</div>
|
||||
)}
|
||||
{screenshot && (
|
||||
<img
|
||||
src={`data:image/${screenshot.format};base64,${screenshot.data}`}
|
||||
alt="Tab screenshot"
|
||||
className="w-full rounded-md border"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* JS Console */}
|
||||
<div className="border-t">
|
||||
<form
|
||||
className="flex items-center gap-2 px-3 py-2"
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
void handleEvaluate();
|
||||
}}
|
||||
>
|
||||
<Terminal className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
<input
|
||||
type="text"
|
||||
value={evalExpr}
|
||||
onChange={(ev) => setEvalExpr(ev.target.value)}
|
||||
placeholder="Evaluate JavaScript..."
|
||||
className="flex-1 bg-transparent text-sm outline-none font-mono"
|
||||
/>
|
||||
<Button variant="ghost" size="sm" className="h-6 px-2 shrink-0" type="submit" disabled={isEvaluating || !evalExpr.trim()}>
|
||||
{isEvaluating ? <Loader2 className="h-3 w-3 animate-spin" /> : 'Run'}
|
||||
</Button>
|
||||
</form>
|
||||
{evalResult !== null && (
|
||||
<pre className="max-h-32 overflow-auto border-t px-3 py-2 text-xs font-mono opacity-70">{evalResult}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user