| 'use client' |
|
|
| import { useState } from 'react' |
| import { Globe, Search, ArrowRight, Loader2, ExternalLink, BookOpen } from 'lucide-react' |
| import ReactMarkdown from 'react-markdown' |
| import remarkGfm from 'remark-gfm' |
|
|
| const QUICK_SEARCHES = [ |
| { label: 'Latest AI News', query: 'latest AI agent developments 2025' }, |
| { label: 'FastAPI Docs', query: 'https://fastapi.tiangolo.com' }, |
| { label: 'Next.js 14', query: 'Next.js 14 app router best practices' }, |
| { label: 'HuggingFace', query: 'https://huggingface.co' }, |
| ] |
|
|
| export default function BrowserPanel() { |
| const [query, setQuery] = useState('') |
| const [result, setResult] = useState('') |
| const [loading, setLoading] = useState(false) |
| const [history, setHistory] = useState<string[]>([]) |
|
|
| const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000' |
|
|
| const search = async (q?: string) => { |
| const searchQuery = q || query.trim() |
| if (!searchQuery || loading) return |
| setLoading(true) |
| setResult('') |
| setHistory(prev => [searchQuery, ...prev.slice(0, 4)]) |
| try { |
| const resp = await fetch(`${apiUrl}/api/v1/browser/research`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query: searchQuery, session_id: 'browser' }), |
| }) |
| if (resp.ok) { |
| const data = await resp.json() |
| setResult(data.result || '') |
| } else { |
| setResult('❌ Research failed. Check if the backend is running.') |
| } |
| } catch (e) { |
| setResult('❌ Cannot reach backend. Make sure the API server is running.') |
| } finally { |
| setLoading(false) |
| } |
| } |
|
|
| return ( |
| <div className="flex flex-col h-full" style={{ background: 'var(--bg-1)' }}> |
| {/* Header */} |
| <div className="px-3 py-2.5 border-b shrink-0" |
| style={{ borderColor: 'var(--border)', background: 'var(--bg-2)' }}> |
| <div className="flex items-center gap-2 mb-2"> |
| <Globe size={13} className="text-blue-400" /> |
| <span className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}> |
| Browser Agent |
| </span> |
| <span className="text-[10px] px-1.5 py-0.5 rounded" |
| style={{ background: 'rgba(59,130,246,0.15)', color: '#3b82f6', border: '1px solid rgba(59,130,246,0.3)' }}> |
| Web Research |
| </span> |
| </div> |
| |
| {/* Search bar */} |
| <div className="flex gap-2"> |
| <div className="flex-1 relative"> |
| <Search size={11} className="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-40" |
| style={{ color: 'var(--text-muted)' }} /> |
| <input |
| value={query} |
| onChange={e => setQuery(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && search()} |
| placeholder="URL or search query..." |
| className="w-full text-xs pl-7 pr-3 py-2 rounded-xl outline-none" |
| style={{ |
| background: 'var(--bg-3)', |
| border: '1px solid var(--border)', |
| color: 'var(--text-primary)', |
| }} |
| /> |
| </div> |
| <button |
| onClick={() => search()} |
| disabled={loading || !query.trim()} |
| className="p-2 rounded-xl transition-all disabled:opacity-40" |
| style={{ background: 'var(--brand)' }}> |
| {loading ? <Loader2 size={13} className="text-white animate-spin" /> : <ArrowRight size={13} className="text-white" />} |
| </button> |
| </div> |
| </div> |
| |
| {/* Quick searches */} |
| <div className="flex gap-1.5 px-3 py-2 flex-wrap shrink-0 border-b" |
| style={{ borderColor: 'var(--border)' }}> |
| {QUICK_SEARCHES.map(({ label, query: q }) => ( |
| <button key={label} |
| onClick={() => { setQuery(q); search(q) }} |
| className="text-[10px] px-2 py-1 rounded-lg transition-all hover:opacity-80" |
| style={{ background: 'var(--bg-3)', border: '1px solid var(--border)', color: 'var(--text-secondary)' }}> |
| {label} |
| </button> |
| ))} |
| </div> |
| |
| {/* Result */} |
| <div className="flex-1 overflow-y-auto p-3"> |
| {loading ? ( |
| <div className="flex flex-col items-center justify-center h-40 gap-3"> |
| <div className="relative"> |
| <Globe size={24} className="text-blue-400 animate-pulse" /> |
| </div> |
| <p className="text-xs animate-pulse" style={{ color: 'var(--text-muted)' }}> |
| Researching the web... |
| </p> |
| </div> |
| ) : result ? ( |
| <div className="prose prose-sm prose-invert max-w-none text-xs" |
| style={{ color: 'var(--text-secondary)' }}> |
| <ReactMarkdown remarkPlugins={[remarkGfm]}>{result}</ReactMarkdown> |
| </div> |
| ) : ( |
| <div className="flex flex-col items-center justify-center h-40 gap-3"> |
| <BookOpen size={24} className="opacity-20" style={{ color: 'var(--text-muted)' }} /> |
| <p className="text-xs text-center" style={{ color: 'var(--text-muted)' }}> |
| Enter a URL or search query<br />to start web research |
| </p> |
| </div> |
| )} |
| </div> |
| |
| {/* History */} |
| {history.length > 0 && ( |
| <div className="px-3 py-2 border-t shrink-0" style={{ borderColor: 'var(--border)', background: 'var(--bg-2)' }}> |
| <p className="text-[10px] mb-1" style={{ color: 'var(--text-muted)' }}>Recent:</p> |
| <div className="flex flex-col gap-0.5"> |
| {history.map((h, i) => ( |
| <button key={i} |
| onClick={() => { setQuery(h); search(h) }} |
| className="text-[10px] text-left truncate hover:opacity-80 transition-all" |
| style={{ color: 'var(--text-secondary)' }}> |
| → {h} |
| </button> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|