@@ -2288,3 +2288,187 @@ export const ThinkingWidget: React.FC<{
22882288 </ div >
22892289 ) ;
22902290} ;
2291+
2292+ /**
2293+ * Widget for WebFetch tool - displays URL fetching with optional prompts
2294+ */
2295+ export const WebFetchWidget : React . FC < {
2296+ url : string ;
2297+ prompt ?: string ;
2298+ result ?: any ;
2299+ } > = ( { url, prompt, result } ) => {
2300+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
2301+ const [ showFullContent , setShowFullContent ] = useState ( false ) ;
2302+
2303+ // Extract result content if available
2304+ let fetchedContent = '' ;
2305+ let isLoading = ! result ;
2306+ let hasError = false ;
2307+
2308+ if ( result ) {
2309+ if ( typeof result . content === 'string' ) {
2310+ fetchedContent = result . content ;
2311+ } else if ( result . content && typeof result . content === 'object' ) {
2312+ if ( result . content . text ) {
2313+ fetchedContent = result . content . text ;
2314+ } else if ( Array . isArray ( result . content ) ) {
2315+ fetchedContent = result . content
2316+ . map ( ( c : any ) => ( typeof c === 'string' ? c : c . text || JSON . stringify ( c ) ) )
2317+ . join ( '\n' ) ;
2318+ } else {
2319+ fetchedContent = JSON . stringify ( result . content , null , 2 ) ;
2320+ }
2321+ }
2322+
2323+ // Check if there's an error
2324+ hasError = result . is_error ||
2325+ fetchedContent . toLowerCase ( ) . includes ( 'error' ) ||
2326+ fetchedContent . toLowerCase ( ) . includes ( 'failed' ) ;
2327+ }
2328+
2329+ // Truncate content for preview
2330+ const maxPreviewLength = 500 ;
2331+ const isTruncated = fetchedContent . length > maxPreviewLength ;
2332+ const previewContent = isTruncated && ! showFullContent
2333+ ? fetchedContent . substring ( 0 , maxPreviewLength ) + '...'
2334+ : fetchedContent ;
2335+
2336+ // Extract domain from URL for display
2337+ const getDomain = ( urlString : string ) => {
2338+ try {
2339+ const urlObj = new URL ( urlString ) ;
2340+ return urlObj . hostname ;
2341+ } catch {
2342+ return urlString ;
2343+ }
2344+ } ;
2345+
2346+ const handleUrlClick = async ( ) => {
2347+ try {
2348+ await open ( url ) ;
2349+ } catch ( error ) {
2350+ console . error ( 'Failed to open URL:' , error ) ;
2351+ }
2352+ } ;
2353+
2354+ return (
2355+ < div className = "flex flex-col gap-2" >
2356+ { /* Header with URL and optional prompt */ }
2357+ < div className = "space-y-2" >
2358+ { /* URL Display */ }
2359+ < div className = "flex items-center gap-2 px-3 py-2 rounded-lg bg-purple-500/5 border border-purple-500/10" >
2360+ < Globe className = "h-4 w-4 text-purple-500/70" />
2361+ < span className = "text-xs font-medium uppercase tracking-wider text-purple-600/70 dark:text-purple-400/70" > Fetching</ span >
2362+ < button
2363+ onClick = { handleUrlClick }
2364+ className = "text-sm text-foreground/80 hover:text-foreground flex-1 truncate text-left hover:underline decoration-purple-500/50"
2365+ >
2366+ { url }
2367+ </ button >
2368+ </ div >
2369+
2370+ { /* Prompt Display */ }
2371+ { prompt && (
2372+ < div className = "ml-6 space-y-1" >
2373+ < button
2374+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
2375+ className = "flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
2376+ >
2377+ < ChevronRight className = { cn ( "h-3 w-3 transition-transform" , isExpanded && "rotate-90" ) } />
2378+ < Info className = "h-3 w-3" />
2379+ < span > Analysis Prompt</ span >
2380+ </ button >
2381+
2382+ { isExpanded && (
2383+ < div className = "rounded-lg border bg-muted/30 p-3 ml-4" >
2384+ < p className = "text-sm text-foreground/90" >
2385+ { prompt }
2386+ </ p >
2387+ </ div >
2388+ ) }
2389+ </ div >
2390+ ) }
2391+ </ div >
2392+
2393+ { /* Results */ }
2394+ { isLoading ? (
2395+ < div className = "rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden" >
2396+ < div className = "px-3 py-2 flex items-center gap-2 text-muted-foreground" >
2397+ < div className = "animate-pulse flex items-center gap-1" >
2398+ < div className = "h-1 w-1 bg-purple-500 rounded-full animate-bounce [animation-delay:-0.3s]" > </ div >
2399+ < div className = "h-1 w-1 bg-purple-500 rounded-full animate-bounce [animation-delay:-0.15s]" > </ div >
2400+ < div className = "h-1 w-1 bg-purple-500 rounded-full animate-bounce" > </ div >
2401+ </ div >
2402+ < span className = "text-sm" > Fetching content from { getDomain ( url ) } ...</ span >
2403+ </ div >
2404+ </ div >
2405+ ) : fetchedContent ? (
2406+ < div className = "rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden" >
2407+ { hasError ? (
2408+ < div className = "px-3 py-2" >
2409+ < div className = "flex items-center gap-2 text-destructive" >
2410+ < AlertCircle className = "h-4 w-4" />
2411+ < span className = "text-sm font-medium" > Failed to fetch content</ span >
2412+ </ div >
2413+ < pre className = "mt-2 text-xs font-mono text-muted-foreground whitespace-pre-wrap" >
2414+ { fetchedContent }
2415+ </ pre >
2416+ </ div >
2417+ ) : (
2418+ < div className = "p-3 space-y-2" >
2419+ { /* Content Header */ }
2420+ < div className = "flex items-center justify-between" >
2421+ < div className = "flex items-center gap-2 text-sm text-muted-foreground" >
2422+ < FileText className = "h-3.5 w-3.5" />
2423+ < span > Content from { getDomain ( url ) } </ span >
2424+ </ div >
2425+ { isTruncated && (
2426+ < button
2427+ onClick = { ( ) => setShowFullContent ( ! showFullContent ) }
2428+ className = "text-xs text-purple-500 hover:text-purple-600 transition-colors flex items-center gap-1"
2429+ >
2430+ { showFullContent ? (
2431+ < >
2432+ < ChevronUp className = "h-3 w-3" />
2433+ Show less
2434+ </ >
2435+ ) : (
2436+ < >
2437+ < ChevronDown className = "h-3 w-3" />
2438+ Show full content
2439+ </ >
2440+ ) }
2441+ </ button >
2442+ ) }
2443+ </ div >
2444+
2445+ { /* Fetched Content */ }
2446+ < div className = "relative" >
2447+ < div className = { cn (
2448+ "rounded-lg bg-muted/30 p-3 overflow-hidden" ,
2449+ ! showFullContent && isTruncated && "max-h-[300px]"
2450+ ) } >
2451+ < pre className = "text-sm font-mono text-foreground/90 whitespace-pre-wrap" >
2452+ { previewContent }
2453+ </ pre >
2454+ { ! showFullContent && isTruncated && (
2455+ < div className = "absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-muted/30 to-transparent pointer-events-none" />
2456+ ) }
2457+ </ div >
2458+ </ div >
2459+ </ div >
2460+ ) }
2461+ </ div >
2462+ ) : (
2463+ < div className = "rounded-lg border bg-background/50 backdrop-blur-sm overflow-hidden" >
2464+ < div className = "px-3 py-2" >
2465+ < div className = "flex items-center gap-2 text-muted-foreground" >
2466+ < Info className = "h-4 w-4" />
2467+ < span className = "text-sm" > No content returned</ span >
2468+ </ div >
2469+ </ div >
2470+ </ div >
2471+ ) }
2472+ </ div >
2473+ ) ;
2474+ } ;
0 commit comments