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
Options
{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)}
>
)}
);
}