import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { apiPath } from "@/lib/api"; import { AlertTriangle, ChevronRight, Eye, EyeOff, FileSearch, Play, RotateCw, TreePine, X, } from "lucide-react"; import { useEffect, useState } from "react"; interface Bounds { x: number; y: number; width: number; height: number; } interface Element { id: number; type: string; name: string; bounds?: Bounds; enabled?: boolean; visible?: boolean; } interface CrawlResult { success: boolean; error?: string; window_handle?: number; element_count?: number; elements?: Element[]; screenshot_base64?: string; screenshot_path?: string; report_path?: string; report_text?: string; output_dir?: string; shortcuts_tested?: number; results?: Array<{ shortcut: string; screenshot?: string }>; } interface CrawlHistory { id: string; timestamp: string; method: string; target: string; element_count: number; window_handle?: number; } function ElementTree({ elements }: { elements: Element[] }) { const types = [...new Set(elements.map((e) => e.type))].sort(); return (
{types.map((type) => { const grouped = elements.filter((e) => e.type === type); return (
{grouped.length} {type}
{grouped.map((el) => (
{el.id} {el.name || `<${el.type.toLowerCase()}>`} {el.bounds && ( {el.bounds.width}×{el.bounds.height} )} {el.enabled === false && ( )}
))}
); })}
); } export function Crawler() { const [activeTab, setActiveTab] = useState("start"); const [windowTitle, setWindowTitle] = useState(""); const [executable, setExecutable] = useState(""); const [pid, setPid] = useState(""); const [maxDepth, setMaxDepth] = useState("5"); const [method, setMethod] = useState("crawl"); const [outputDir, setOutputDir] = useState(""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(""); const [history, setHistory] = useState([]); const [selectedReport, setSelectedReport] = useState( null, ); const [imageModal, setImageModal] = useState(null); useEffect(() => { const stored = localStorage.getItem("analyze_winapp_history"); if (stored) { try { setHistory(JSON.parse(stored)); } catch { /* ignore */ } } }, []); const saveToHistory = (res: CrawlResult) => { const entry: CrawlHistory = { id: Date.now().toString(36), timestamp: new Date().toISOString(), method: method, target: windowTitle || executable || pid || "unknown", element_count: res.element_count ?? 0, window_handle: res.window_handle, }; const updated = [entry, ...history].slice(0, 50); setHistory(updated); localStorage.setItem("analyze_winapp_history", JSON.stringify(updated)); }; const handleStartCrawl = async () => { setLoading(true); setError(""); setResult(null); const params: Record = { operation: method, max_depth: Number.parseInt(maxDepth) || 5, }; if (windowTitle.trim()) params.window_title = windowTitle.trim(); if (outputDir.trim()) params.output_dir = outputDir.trim(); if (pid.trim()) params.window_handle = Number.parseInt(pid.trim()); try { const resp = await fetch(apiPath("/api/v1/tools/call"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "analyze_winapp", arguments: { request: params }, }), }); const data = await resp.json(); if (data?.result?.content?.[0]?.text) { const parsed: CrawlResult = JSON.parse(data.result.content[0].text); setResult(parsed); if (parsed.success) { saveToHistory(parsed); } else { setError(parsed.error ?? "Crawl failed"); } } else if (data?.error) { setError(data.error.message || "MCP error"); } } catch (e) { setError(String(e)); } finally { setLoading(false); } }; const loadReport = async (entry: CrawlHistory) => { if (entry.window_handle) { try { const resp = await fetch(apiPath("/api/v1/tools/call"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "analyze_winapp", arguments: { request: { operation: "crawl", window_handle: entry.window_handle, }, }, }), }); const data = await resp.json(); if (data?.result?.content?.[0]?.text) { setSelectedReport(JSON.parse(data.result.content[0].text)); } } catch { setSelectedReport(null); } } }; return (

App Crawler

Crawl Windows apps, discover UI elements, and generate reports

Start Crawl Reports ({history.length}) Tree Viewer {/* ── Start Crawl ── */}

Target

setWindowTitle(e.target.value)} placeholder="e.g. Wbridge5, Notepad" className="border-slate-700 bg-slate-900 text-slate-200" />

Searches for window by title substring

setExecutable(e.target.value)} placeholder="C:\Programs\App\app.exe" className="border-slate-700 bg-slate-900 text-slate-200" />

Launches app if not running

setPid(e.target.value)} placeholder="e.g. 90124" className="border-slate-700 bg-slate-900 text-slate-200" />

Attach to running process by PID

setOutputDir(e.target.value)} placeholder="defaults to ./winapp_analysis/" className="border-slate-700 bg-slate-900 text-slate-200" />

Options

setMaxDepth(e.target.value)} className="border-slate-700 bg-slate-900 text-slate-200" />
{error && (
{error}
)} {result && result.success && (

Crawl Complete

Elements

{result.element_count}

Window

{result.window_handle}

{result.report_path && (
Report

{result.report_path}

)}
{result.elements && result.elements.length > 0 && (
)}
)}
{/* ── Reports ── */} {history.length === 0 ? (

No crawl reports yet.

Start a crawl from the tab above.

) : (
{history.map((entry) => ( loadReport(entry)} >
{entry.method}

{entry.target}

{new Date(entry.timestamp).toLocaleString()} ·{" "} {entry.element_count} elements

))}
)}
{/* ── Tree Viewer ── */} {selectedReport?.elements && selectedReport.elements.length > 0 ? (

Element Tree ({selectedReport.elements.length})

{selectedReport.screenshot_base64 && ( )}
) : selectedReport && !selectedReport.success ? ( {selectedReport.error} ) : (

No element tree loaded.

Run a crawl or select a report to view its element tree.

)} {imageModal && (
setImageModal(null)} >
Crawl screenshot
)}
); }