Spaces:
Runtime error
Runtime error
import React, { useState, useEffect, useRef } from 'react'; | |
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; | |
type ChatMessage = { | |
role: 'user' | 'system'; | |
content: string; | |
}; | |
const App: React.FC = () => { | |
const [urlInput, setUrlInput] = useState<string>('https://www.example.com'); | |
const [bulkToggle, setBulkToggle] = useState<boolean>(false); | |
const [actionRadio, setActionRadio] = useState<'Scrape data' | 'Capture image' | 'Both'>('Both'); | |
const [maxUrls, setMaxUrls] = useState<number>(5); | |
const [crawlDepth, setCrawlDepth] = useState<number>(1); | |
const [scrapedDataOutput, setScrapedDataOutput] = useState<string>(''); | |
const [screenshotOutput, setScreenshotOutput] = useState<string | null>(null); | |
const [monitorUrlInput, setMonitorUrlInput] = useState<string>(''); | |
const [intervalInput, setIntervalInput] = useState<number>(300); | |
const [changeOutput, setChangeOutput] = useState<string>(''); | |
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]); | |
const [isMonitoring, setIsMonitoring] = useState<boolean>(false); | |
const [monitoringData, setMonitoringData] = useState<{ time: string; changes: number }[]>([]); | |
const [isProcessing, setIsProcessing] = useState<boolean>(false); | |
const [error, setError] = useState<string | null>(null); | |
const wsRef = useRef<WebSocket | null>(null); | |
useEffect(() => { | |
let ws: WebSocket | null = null; | |
if (isMonitoring) { | |
ws = new WebSocket('ws://localhost:8000/ws'); | |
wsRef.current = ws; | |
ws.onopen = () => { | |
console.log("Connected to WebSocket server."); | |
}; | |
ws.onmessage = (event) => { | |
const message = JSON.parse(event.data); | |
if (message.type === "change_detected") { | |
setChangeOutput(prev => prev + `Change detected at ${message.url}\n`); | |
setChatHistory(prev => [...prev, { role: 'system', content: `Change detected at ${message.url}` }]); | |
setMonitoringData(prev => { | |
const now = new Date(); | |
const time = now.toLocaleTimeString(); | |
return [...prev, { time, changes: 1 }]; | |
}); | |
} else if (message.type === "server_log") { | |
console.log("Server Log:", message.log); | |
} else if (message.type === "server_error") { | |
setError(message.error); | |
} | |
}; | |
ws.onclose = () => { | |
console.log("Disconnected from WebSocket server."); | |
}; | |
ws.onerror = (error) => { | |
console.error("WebSocket error:", error); | |
setError("WebSocket connection error."); | |
}; | |
} else if (wsRef.current) { | |
wsRef.current.close(); | |
wsRef.current = null; | |
} | |
return () => { | |
if (wsRef.current) { | |
wsRef.current.close(); | |
} | |
}; | |
}, [isMonitoring]); | |
const handleProcessUrls = async () => { | |
setIsProcessing(true); | |
setError(null); | |
try { | |
const response = await fetch('http://localhost:8000/process_urls', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
url_input: urlInput, | |
bulk_toggle: bulkToggle, | |
action_radio: actionRadio, | |
max_urls: maxUrls, | |
crawl_depth: crawlDepth, | |
}), | |
}); | |
if (!response.ok) { | |
const errorData = await response.json(); | |
throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || 'Unknown error'}`); | |
} | |
const data = await response.json(); | |
setScrapedDataOutput(JSON.stringify(data.scraped_data, null, 2)); | |
if (data.screenshot_data) { | |
setScreenshotOutput(data.screenshot_data); | |
} else { | |
setScreenshotOutput(null); | |
} | |
setError(null); | |
} catch (e: any) { | |
console.error("Error processing URLs:", e); | |
setError(e.message); | |
setScrapedDataOutput(''); | |
setScreenshotOutput(null); | |
} finally { | |
setIsProcessing(false); | |
} | |
}; | |
const handleChatSubmit = async (user_input: string) => { | |
setChatHistory(prev => [...prev, { role: 'user', content: user_input }]); | |
if (user_input.toLowerCase().includes("start monitoring")) { | |
setIsMonitoring(true); | |
setChatHistory(prev => [...prev, { role: 'system', content: "Monitoring started." }]); | |
} else if (user_input.toLowerCase().includes("stop monitoring")) { | |
setIsMonitoring(false); | |
setChatHistory(prev => [...prev, { role: 'system', content: "Monitoring stopped." }]); | |
} else if (user_input.toLowerCase().includes("help")) { | |
setChatHistory(prev => [...prev, { role: 'system', content: "Commands: 'Start monitoring', 'Stop monitoring', 'Help'" }]); | |
} else { | |
setChatHistory(prev => [...prev, { role: 'system', content: "Unknown command. Type 'Help' for available commands." }]); | |
} | |
}; | |
const handleStartMonitoring = () => { | |
setIsMonitoring(true); | |
setChatHistory(prev => [...prev, { role: 'system', content: "Monitoring started." }]); | |
}; | |
const handleStopMonitoring = () => { | |
setIsMonitoring(false); | |
setChatHistory(prev => [...prev, { role: 'system', content: "Monitoring stopped." }]); | |
setMonitoringData([]); | |
}; | |
return ( | |
<div className="bg-gray-100 min-h-screen p-4"> | |
<h1 className="text-3xl font-bold text-center text-gray-800 mb-8">Smart Scraper with Change Detection</h1> | |
{error && <div className="bg-red-200 text-red-700 rounded-md p-2 mb-4">{error}</div>} | |
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4"> | |
<div className="bg-white rounded-lg shadow-md p-4 flex-1"> | |
<h2 className="text-xl font-semibold mb-4 text-gray-700">URL Scrape/Screenshot</h2> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Enter URL(s)</label> | |
<textarea | |
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" | |
value={urlInput} | |
onChange={(e) => setUrlInput(e.target.value)} | |
/> | |
</div> | |
<div className="flex items-center space-x-4 mb-2"> | |
<label className="block text-gray-700 text-sm font-bold">Bulk URLs</label> | |
<input type="checkbox" className="form-checkbox h-5 w-5 text-indigo-600" checked={bulkToggle} onChange={() => setBulkToggle(!bulkToggle)} /> | |
</div> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Select Action</label> | |
<div className="space-x-4 flex"> | |
<label className="flex items-center space-x-2"> | |
<input type="radio" className="form-radio h-4 w-4 text-indigo-600" value="Scrape data" checked={actionRadio === "Scrape data"} onChange={() => setActionRadio("Scrape data")} /> | |
<span className="text-gray-700 text-sm">Scrape data</span> | |
</label> | |
<label className="flex items-center space-x-2"> | |
<input type="radio" className="form-radio h-4 w-4 text-indigo-600" value="Capture image" checked={actionRadio === "Capture image"} onChange={() => setActionRadio("Capture image")} /> | |
<span className="text-gray-700 text-sm">Capture image</span> | |
</label> | |
<label className="flex items-center space-x-2"> | |
<input type="radio" className="form-radio h-4 w-4 text-indigo-600" value="Both" checked={actionRadio === "Both"} onChange={() => setActionRadio("Both")} /> | |
<span className="text-gray-700 text-sm">Both</span> | |
</label> | |
</div> | |
</div> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Max URLs to process</label> | |
<input type="range" className="form-range w-full" min={1} max={1000} value={maxUrls} onChange={(e) => setMaxUrls(parseInt(e.target.value))} /> | |
<span className="text-sm text-gray-600">{maxUrls}</span> | |
</div> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Crawl Depth</label> | |
<input type="range" className="form-range w-full" min={1} max={3} value={crawlDepth} onChange={(e) => setCrawlDepth(parseInt(e.target.value))} /> | |
<span className="text-sm text-gray-600">{crawlDepth}</span> | |
</div> | |
<button | |
className={`bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`} | |
onClick={handleProcessUrls} | |
disabled={isProcessing} | |
> | |
{isProcessing ? 'Processing...' : 'Process URLs'} | |
</button> | |
{scrapedDataOutput && ( | |
<div className="mt-4"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Scraped Data</label> | |
<pre className="border border-gray-300 rounded-md bg-gray-50 p-2 overflow-auto max-h-48 whitespace-pre-wrap"> | |
{scrapedDataOutput} | |
</pre> | |
</div> | |
)} | |
{screenshotOutput && ( | |
<div className="mt-4"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Screenshot</label> | |
<img src={screenshotOutput} alt="Screenshot" className="rounded-lg shadow-lg max-h-96" /> | |
</div> | |
)} | |
{!screenshotOutput && actionRadio === 'Capture image' && ( | |
<div className="mt-4"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Screenshot</label> | |
<div className="bg-gray-200 border-2 border-dashed rounded-xl w-48 h-48 flex items-center justify-center"> | |
<span className="text-gray-500">No image</span> | |
</div> | |
</div> | |
)} | |
</div> | |
<div className="bg-white rounded-lg shadow-md p-4 flex-1"> | |
<h2 className="text-xl font-semibold mb-4 text-gray-700">Monitoring</h2> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Enter URLs to Monitor (separated by newline)</label> | |
<textarea | |
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" | |
value={monitorUrlInput} | |
onChange={(e) => setMonitorUrlInput(e.target.value)} | |
/> | |
</div> | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Monitoring Interval (seconds)</label> | |
<input type="range" className="form-range w-full" min={1} max={3600} value={intervalInput} onChange={(e) => setIntervalInput(parseInt(e.target.value))} /> | |
<span className="text-sm text-gray-600">{intervalInput}</span> | |
</div> | |
<div className="flex space-x-4 mb-4"> | |
<button | |
className={`bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ${isMonitoring ? 'opacity-50 cursor-not-allowed' : ''}`} | |
onClick={handleStartMonitoring} | |
disabled={isMonitoring} | |
> | |
Start Monitoring | |
</button> | |
<button | |
className={`bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ${!isMonitoring ? 'opacity-50 cursor-not-allowed' : ''}`} | |
onClick={handleStopMonitoring} | |
disabled={!isMonitoring} | |
> | |
Stop Monitoring | |
</button> | |
</div> | |
{changeOutput && ( | |
<div className="mb-4"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Monitoring Changes</label> | |
<pre className="border border-gray-300 rounded-md bg-gray-50 p-2 overflow-auto max-h-48 whitespace-pre-wrap"> | |
{changeOutput} | |
</pre> | |
</div> | |
)} | |
{monitoringData.length > 0 && ( | |
<div className="mb-4"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Change History Graph</label> | |
<ResponsiveContainer width="100%" height={200}> | |
<LineChart data={monitoringData}> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis dataKey="time" /> | |
<YAxis /> | |
<Tooltip /> | |
<Legend /> | |
<Line type="monotone" dataKey="changes" stroke="#8884d8" /> | |
</LineChart> | |
</ResponsiveContainer> | |
</div> | |
)} | |
<div className="mb-2"> | |
<label className="block text-gray-700 text-sm font-bold mb-2">Monitoring Chat</label> | |
<div className="border rounded-md bg-gray-50 p-2 overflow-auto max-h-48 mb-2"> | |
<ul className="space-y-2"> | |
{chatHistory.map((msg, index) => ( | |
<li key={index} className={msg.role === 'user' ? 'text-right' : 'text-left'}> | |
<div className={`${msg.role === 'user' ? 'bg-indigo-100' : 'bg-gray-100'} inline-block rounded-md p-2`}> | |
<span className="font-bold text-gray-700">{msg.role === 'user' ? 'You' : 'System'}:</span> <span className="text-gray-800">{msg.content}</span> | |
</div> | |
</li> | |
))} | |
</ul> | |
</div> | |
<input | |
type="text" | |
placeholder="Type command" | |
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" | |
onKeyDown={(e) => { | |
if (e.key === 'Enter') { | |
handleChatSubmit(e.target.value); | |
(e.target as HTMLInputElement).value = ''; | |
} | |
}} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default App; |