diff --git a/backend/app/config.py b/backend/app/config.py index 825130e..9368211 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -3,10 +3,11 @@ from typing import Optional class Settings(BaseSettings): - ollama_base_url: str = "http://192.168.30.36:11434" + ollama_base_url: str = "http://localhost:11435" default_model: str = "qwen3:8b" openai_api_key: Optional[str] = None openai_base_url: Optional[str] = None + lens_api_token: Optional[str] = None class Config: env_file = ".env" diff --git a/backend/app/routers/patent_search.py b/backend/app/routers/patent_search.py index 2c1aff6..f296198 100644 --- a/backend/app/routers/patent_search.py +++ b/backend/app/routers/patent_search.py @@ -1,4 +1,4 @@ -"""Patent Search Router - Search for similar patents""" +"""Patent Search Router - Search for similar patents using Lens.org API""" import logging from typing import Optional, List @@ -21,16 +21,20 @@ class PatentSearchRequest(BaseModel): class PatentResult(BaseModel): - """Single patent result""" - publication_number: str + """Single patent result from Lens.org""" + lens_id: str + doc_number: str + jurisdiction: str + kind: str title: str - snippet: str - publication_date: Optional[str] = None - assignee: Optional[str] = None - inventor: Optional[str] = None - status: str # ACTIVE, NOT_ACTIVE, UNKNOWN - pdf_url: Optional[str] = None - thumbnail_url: Optional[str] = None + abstract: Optional[str] = None + date_published: Optional[str] = None + applicants: List[str] = [] + inventors: List[str] = [] + legal_status: Optional[str] = None + classifications_cpc: List[str] = [] + families_simple: List[str] = [] + url: str class PatentSearchResponse(BaseModel): @@ -68,7 +72,7 @@ async def search_patents(request: PatentSearchRequest): """ Search for patents similar to the given description/query. - Uses Google Patents to find related patents based on keywords. + Uses Lens.org API to find related patents based on title, abstract, and claims. """ logger.info(f"Patent search request: {request.query[:100]}...") diff --git a/backend/app/services/patent_search_service.py b/backend/app/services/patent_search_service.py index 645201d..52d3cdc 100644 --- a/backend/app/services/patent_search_service.py +++ b/backend/app/services/patent_search_service.py @@ -1,74 +1,48 @@ -"""Patent Search Service using Google Patents XHR API""" +"""Patent Search Service using Lens.org API""" import httpx import logging -from typing import List, Optional -from urllib.parse import quote_plus +from typing import List, Optional, Dict, Any +from dataclasses import dataclass, asdict + +from app.config import settings logger = logging.getLogger(__name__) +@dataclass class PatentSearchResult: - """Single patent search result""" - def __init__( - self, - publication_number: str, - title: str, - snippet: str, - publication_date: Optional[str], - assignee: Optional[str], - inventor: Optional[str], - status: str, - pdf_url: Optional[str] = None, - thumbnail_url: Optional[str] = None, - ): - self.publication_number = publication_number - self.title = title - self.snippet = snippet - self.publication_date = publication_date - self.assignee = assignee - self.inventor = inventor - self.status = status - self.pdf_url = pdf_url - self.thumbnail_url = thumbnail_url + """Single patent search result from Lens.org""" + lens_id: str + doc_number: str + jurisdiction: str + kind: str + title: str + abstract: Optional[str] + date_published: Optional[str] + applicants: List[str] + inventors: List[str] + legal_status: Optional[str] + classifications_cpc: List[str] + families_simple: List[str] + url: str - def to_dict(self): - return { - "publication_number": self.publication_number, - "title": self.title, - "snippet": self.snippet, - "publication_date": self.publication_date, - "assignee": self.assignee, - "inventor": self.inventor, - "status": self.status, - "pdf_url": self.pdf_url, - "thumbnail_url": self.thumbnail_url, - } + def to_dict(self) -> Dict[str, Any]: + return asdict(self) class PatentSearchService: - """Service for searching patents using Google Patents""" + """Service for searching patents using Lens.org API""" - GOOGLE_PATENTS_XHR_URL = "https://patents.google.com/xhr/query" - GOOGLE_PATENTS_PDF_BASE = "https://patentimages.storage.googleapis.com/" + LENS_API_URL = "https://api.lens.org/patent/search" def __init__(self): self._client: Optional[httpx.AsyncClient] = None - # Browser-like headers to avoid being blocked - DEFAULT_HEADERS = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Accept": "application/json, text/plain, */*", - "Accept-Language": "en-US,en;q=0.9", - "Referer": "https://patents.google.com/", - "Origin": "https://patents.google.com", - } - async def _get_client(self) -> httpx.AsyncClient: if self._client is None or self._client.is_closed: self._client = httpx.AsyncClient( timeout=30.0, - headers=self.DEFAULT_HEADERS, follow_redirects=True, ) return self._client @@ -77,16 +51,27 @@ class PatentSearchService: if self._client and not self._client.is_closed: await self._client.aclose() + def _get_headers(self) -> Dict[str, str]: + """Get headers with authorization token""" + token = settings.lens_api_token + if not token: + raise ValueError("LENS_API_TOKEN environment variable is not set") + return { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + async def search( self, query: str, max_results: int = 10, ) -> dict: """ - Search Google Patents for relevant patents + Search Lens.org for relevant patents Args: - query: Search query (can be a description or keywords) + query: Search query (searches title, abstract, and claims) max_results: Maximum number of results to return Returns: @@ -95,16 +80,39 @@ class PatentSearchService: try: client = await self._get_client() - # URL encode the query - encoded_query = quote_plus(query) - url = f"{self.GOOGLE_PATENTS_XHR_URL}?url=q%3D{encoded_query}&exp=&tags=" + # Build Lens.org query using query string format for full-text search + request_body = { + "query": query, + "size": max_results, + "sort": [{"_score": "desc"}] + } - logger.info(f"Searching patents with query: {query[:100]}...") + logger.info(f"Searching Lens.org patents with query: {query[:100]}...") - response = await client.get(url) + response = await client.post( + self.LENS_API_URL, + json=request_body, + headers=self._get_headers(), + ) + + if response.status_code == 401: + logger.error("Lens.org API authentication failed - check LENS_API_TOKEN") + return { + "total_results": 0, + "patents": [], + "error": "Authentication failed - invalid API token" + } + + if response.status_code == 429: + logger.warning("Lens.org API rate limit exceeded") + return { + "total_results": 0, + "patents": [], + "error": "Rate limit exceeded - please try again later" + } if response.status_code != 200: - logger.error(f"Google Patents API returned status {response.status_code}") + logger.error(f"Lens.org API returned status {response.status_code}: {response.text}") return { "total_results": 0, "patents": [], @@ -112,56 +120,28 @@ class PatentSearchService: } data = response.json() - - # Parse results - results = data.get("results", {}) - total_num = results.get("total_num_results", 0) - clusters = results.get("cluster", []) + total_results = data.get("total", 0) + results = data.get("data", []) patents: List[PatentSearchResult] = [] + for item in results: + patent = self._parse_patent(item) + patents.append(patent) - if clusters and len(clusters) > 0: - patent_results = clusters[0].get("result", []) - - for item in patent_results[:max_results]: - patent_data = item.get("patent", {}) - family_meta = patent_data.get("family_metadata", {}) - aggregated = family_meta.get("aggregated", {}) - country_status = aggregated.get("country_status", []) - - status = "UNKNOWN" - if country_status and len(country_status) > 0: - best_stage = country_status[0].get("best_patent_stage", {}) - status = best_stage.get("state", "UNKNOWN") - - # Build PDF URL if available - pdf_path = patent_data.get("pdf", "") - pdf_url = f"{self.GOOGLE_PATENTS_PDF_BASE}{pdf_path}" if pdf_path else None - - # Build thumbnail URL - thumbnail = patent_data.get("thumbnail", "") - thumbnail_url = f"{self.GOOGLE_PATENTS_PDF_BASE}{thumbnail}" if thumbnail else None - - patent = PatentSearchResult( - publication_number=patent_data.get("publication_number", ""), - title=self._clean_html(patent_data.get("title", "")), - snippet=self._clean_html(patent_data.get("snippet", "")), - publication_date=patent_data.get("publication_date"), - assignee=patent_data.get("assignee"), - inventor=patent_data.get("inventor"), - status=status, - pdf_url=pdf_url, - thumbnail_url=thumbnail_url, - ) - patents.append(patent) - - logger.info(f"Found {total_num} total patents, returning {len(patents)}") + logger.info(f"Found {total_results} total patents, returning {len(patents)}") return { - "total_results": total_num, + "total_results": total_results, "patents": [p.to_dict() for p in patents], } + except ValueError as e: + logger.error(f"Configuration error: {e}") + return { + "total_results": 0, + "patents": [], + "error": str(e) + } except httpx.HTTPError as e: logger.error(f"HTTP error searching patents: {e}") return { @@ -177,18 +157,107 @@ class PatentSearchService: "error": str(e) } - def _clean_html(self, text: str) -> str: - """Remove HTML entities and tags from text""" - if not text: + def _parse_patent(self, item: Dict[str, Any]) -> PatentSearchResult: + """Parse a single patent result from Lens.org response""" + lens_id = item.get("lens_id", "") + jurisdiction = item.get("jurisdiction", "") + doc_number = item.get("doc_number", "") + kind = item.get("kind", "") + + # Get biblio section (contains title, parties, classifications) + biblio = item.get("biblio", {}) + + # Extract title from biblio.invention_title (list with lang info) + title_data = biblio.get("invention_title", []) + title = self._extract_text_with_lang(title_data) + + # Extract abstract (top-level, list with lang info) + abstract_data = item.get("abstract", []) + abstract = self._extract_text_with_lang(abstract_data) + + # Extract applicants from biblio.parties.applicants + parties = biblio.get("parties", {}) + applicants = [] + applicant_data = parties.get("applicants", []) + if isinstance(applicant_data, list): + for app in applicant_data: + if isinstance(app, dict): + name = app.get("extracted_name", {}).get("value", "") + if name: + applicants.append(name) + + # Extract inventors from biblio.parties.inventors + inventors = [] + inventor_data = parties.get("inventors", []) + if isinstance(inventor_data, list): + for inv in inventor_data: + if isinstance(inv, dict): + name = inv.get("extracted_name", {}).get("value", "") + if name: + inventors.append(name) + + # Extract legal status + legal_status_data = item.get("legal_status", {}) + legal_status = None + if isinstance(legal_status_data, dict): + legal_status = legal_status_data.get("patent_status") + + # Extract CPC classifications from biblio.classifications_cpc + classifications_cpc = [] + cpc_data = biblio.get("classifications_cpc", []) + if isinstance(cpc_data, list): + for cpc in cpc_data: + if isinstance(cpc, dict): + symbol = cpc.get("symbol", "") + if symbol: + classifications_cpc.append(symbol) + + # Extract simple family members + families_simple = [] + families_data = item.get("families", {}) + if isinstance(families_data, dict): + simple_family = families_data.get("simple", {}) + if isinstance(simple_family, dict): + members = simple_family.get("members", []) + if isinstance(members, list): + families_simple = [m.get("lens_id", "") for m in members if isinstance(m, dict) and m.get("lens_id")] + + # Build URL to Lens.org patent page + url = f"https://www.lens.org/lens/patent/{lens_id}" if lens_id else "" + + return PatentSearchResult( + lens_id=lens_id, + doc_number=doc_number, + jurisdiction=jurisdiction, + kind=kind, + title=title, + abstract=abstract, + date_published=item.get("date_published"), + applicants=applicants, + inventors=inventors, + legal_status=legal_status, + classifications_cpc=classifications_cpc, + families_simple=families_simple, + url=url, + ) + + def _extract_text_with_lang(self, data: Any, prefer_lang: str = "en") -> str: + """Extract text from Lens.org language-tagged list, preferring specified language""" + if not data: return "" - # Replace common HTML entities - text = text.replace("…", "...") - text = text.replace("&", "&") - text = text.replace("<", "<") - text = text.replace(">", ">") - text = text.replace(""", '"') - text = text.replace("'", "'") - return text.strip() + if isinstance(data, str): + return data + if isinstance(data, list) and data: + # Prefer specified language + for item in data: + if isinstance(item, dict) and item.get("lang") == prefer_lang: + return item.get("text", "") + # Fall back to first item + first = data[0] + if isinstance(first, dict): + return first.get("text", "") + return str(first) + return "" # Singleton instance diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95fb22c..fa722c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -155,7 +155,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2446,7 +2445,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2457,7 +2455,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2518,7 +2515,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -2802,7 +2798,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2971,7 +2966,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3442,7 +3436,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -3531,8 +3524,7 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3646,7 +3638,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4376,7 +4367,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4503,7 +4493,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4513,7 +4502,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4767,7 +4755,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4863,7 +4850,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4985,7 +4971,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cddaadd..c869a5d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -489,6 +489,37 @@ function App() { availableModels={availableModels} /> )} + {activeTab === 'patent' && ( +
+ + + Patent Search Info + + + Search patents using the Lens.org API to find prior art and similar inventions. + + + How to Use + + +
    +
  1. Click a generated description on the left to load it into the search box
  2. +
  3. Edit the description to refine your search query
  4. +
  5. Click "Search Patents" to find similar patents
  6. +
  7. Results appear on the right - click to view on Lens.org
  8. +
+
+ + Result Interpretation + + + Many results: Query may overlap with existing prior art - consider making it more specific. + + + Few/no results: Potentially novel concept - good candidate for further exploration. + +
+ )} {activeTab === 'deduplication' && (
diff --git a/frontend/src/components/PatentSearchPanel.tsx b/frontend/src/components/PatentSearchPanel.tsx index 3ce1b64..b39a565 100644 --- a/frontend/src/components/PatentSearchPanel.tsx +++ b/frontend/src/components/PatentSearchPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Card, Button, @@ -10,17 +10,24 @@ import { List, Tooltip, message, + Badge, } from 'antd'; import { SearchOutlined, LinkOutlined, CopyOutlined, DeleteOutlined, - GlobalOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ClockCircleOutlined, + QuestionCircleOutlined, + EditOutlined, } from '@ant-design/icons'; import type { ExpertTransformationDescription, + PatentResult, } from '../types'; +import { searchPatents } from '../services/api'; const { Text, Paragraph } = Typography; const { TextArea } = Input; @@ -30,315 +37,402 @@ interface PatentSearchPanelProps { isDark: boolean; } -interface SearchItem { +interface SearchResultItem { id: string; query: string; - searchUrl: string; expertName?: string; keyword?: string; + loading: boolean; + error?: string; + totalResults: number; + patents: PatentResult[]; } -// Generate Google Patents search URL -function generatePatentSearchUrl(query: string): string { - // Extract key terms and create a search-friendly query - const encodedQuery = encodeURIComponent(query); - return `https://patents.google.com/?q=${encodedQuery}`; -} - -// Generate Lens.org search URL (alternative) -function generateLensSearchUrl(query: string): string { - const encodedQuery = encodeURIComponent(query); - return `https://www.lens.org/lens/search/patent/list?q=${encodedQuery}`; +// Get status icon and color +function getStatusDisplay(status: string | null): { icon: React.ReactNode; color: string; text: string } { + switch (status) { + case 'ACTIVE': + return { icon: , color: 'green', text: 'Active' }; + case 'PENDING': + return { icon: , color: 'blue', text: 'Pending' }; + case 'DISCONTINUED': + case 'EXPIRED': + return { icon: , color: 'red', text: status }; + default: + return { icon: , color: 'default', text: status || 'Unknown' }; + } } export function PatentSearchPanel({ descriptions, isDark }: PatentSearchPanelProps) { const [customQuery, setCustomQuery] = useState(''); - const [searchItems, setSearchItems] = useState([]); - const [selectedDescriptions, setSelectedDescriptions] = useState>(new Set()); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [apiStatus, setApiStatus] = useState<'checking' | 'connected' | 'error'>('checking'); - // Add custom query to search list - const handleAddCustomQuery = useCallback(() => { + // Check API connection on mount + useEffect(() => { + const checkApi = async () => { + try { + const res = await fetch(`http://${window.location.hostname}:8001/health`); + setApiStatus(res.ok ? 'connected' : 'error'); + } catch { + setApiStatus('error'); + } + }; + checkApi(); + }, []); + + // Search patents for a query + const doSearch = useCallback(async ( + query: string, + expertName?: string, + keyword?: string + ): Promise => { + const id = `search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + try { + const response = await searchPatents({ query, max_results: 10 }); + + return { + id, + query, + expertName, + keyword, + loading: false, + totalResults: response.total_results, + patents: response.patents, + error: response.error || undefined, + }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Search failed'; + + return { + id, + query, + expertName, + keyword, + loading: false, + totalResults: 0, + patents: [], + error: `${errorMsg} (API: ${window.location.hostname}:8001)`, + }; + } + }, []); + + // Handle custom query search + const handleSearchCustom = useCallback(async () => { if (!customQuery.trim()) return; - const newItem: SearchItem = { - id: `custom-${Date.now()}`, - query: customQuery.trim(), - searchUrl: generatePatentSearchUrl(customQuery.trim()), - }; - - setSearchItems(prev => [newItem, ...prev]); + setIsSearching(true); + const result = await doSearch(customQuery.trim()); + setSearchResults(prev => [result, ...prev]); setCustomQuery(''); - message.success('Added to search list'); - }, [customQuery]); + setIsSearching(false); - // Add selected descriptions to search list - const handleAddSelected = useCallback(() => { - if (!descriptions || selectedDescriptions.size === 0) return; + if (result.error) { + message.error(`Search failed: ${result.error}`); + } else { + message.success(`Found ${result.totalResults.toLocaleString()} patents (${result.patents.length} returned)`); + } + }, [customQuery, doSearch]); - const newItems: SearchItem[] = Array.from(selectedDescriptions).map(idx => { - const desc = descriptions[idx]; - return { - id: `desc-${idx}-${Date.now()}`, - query: desc.description, - searchUrl: generatePatentSearchUrl(desc.description), - expertName: desc.expert_name, - keyword: desc.keyword, - }; - }); - - setSearchItems(prev => [...newItems, ...prev]); - setSelectedDescriptions(new Set()); - message.success(`Added ${newItems.length} items to search list`); - }, [descriptions, selectedDescriptions]); - - // Remove item from list - const handleRemoveItem = useCallback((id: string) => { - setSearchItems(prev => prev.filter(item => item.id !== id)); + // Handle clicking a generated description - put it in search input + const handleSelectDescription = useCallback((desc: ExpertTransformationDescription) => { + setCustomQuery(desc.description); + message.info('Description loaded into search box - edit and search when ready'); }, []); - // Copy URL to clipboard - const handleCopyUrl = useCallback((url: string) => { - navigator.clipboard.writeText(url); - message.success('URL copied to clipboard'); + // Remove result from list + const handleRemoveResult = useCallback((id: string) => { + setSearchResults(prev => prev.filter(item => item.id !== id)); }, []); - // Toggle description selection - const toggleDescription = useCallback((index: number) => { - setSelectedDescriptions(prev => { - const next = new Set(prev); - if (next.has(index)) { - next.delete(index); - } else { - next.add(index); - } - return next; - }); + // Copy patent info to clipboard + const handleCopyPatent = useCallback((patent: PatentResult) => { + const text = `${patent.title}\n${patent.jurisdiction}-${patent.doc_number}\n${patent.url}`; + navigator.clipboard.writeText(text); + message.success('Patent info copied'); }, []); - // Clear all + // Clear all results const handleClearAll = useCallback(() => { - setSearchItems([]); + setSearchResults([]); }, []); - const containerStyle: React.CSSProperties = { - height: '100%', - display: 'flex', - flexDirection: 'column', - gap: 16, - padding: 16, - overflow: 'auto', - }; - const cardStyle: React.CSSProperties = { background: isDark ? '#1f1f1f' : '#fff', }; return ( -
- {/* Info banner */} - - - - - Generate search links to check for similar patents on Google Patents or Lens.org - - - - - {/* Custom search input */} - -