feat: Enhance patent search and update research documentation

- Improve patent search service with expanded functionality
- Update PatentSearchPanel UI component
- Add new research_report.md
- Update experimental protocol, literature review, paper outline, and theoretical framework

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 15:52:33 +08:00
parent ec48709755
commit 26a56a2a07
13 changed files with 1446 additions and 537 deletions

View File

@@ -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"
}

View File

@@ -489,6 +489,37 @@ function App() {
availableModels={availableModels}
/>
)}
{activeTab === 'patent' && (
<div style={{ padding: 16 }}>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
<FileSearchOutlined style={{ marginRight: 8 }} />
Patent Search Info
</Typography.Title>
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
Search patents using the Lens.org API to find prior art and similar inventions.
</Typography.Paragraph>
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>
How to Use
</Typography.Title>
<Typography.Paragraph style={{ fontSize: 12 }}>
<ol style={{ paddingLeft: 16, margin: 0 }}>
<li style={{ marginBottom: 8 }}>Click a generated description on the left to load it into the search box</li>
<li style={{ marginBottom: 8 }}>Edit the description to refine your search query</li>
<li style={{ marginBottom: 8 }}>Click "Search Patents" to find similar patents</li>
<li style={{ marginBottom: 8 }}>Results appear on the right - click to view on Lens.org</li>
</ol>
</Typography.Paragraph>
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>
Result Interpretation
</Typography.Title>
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
<strong>Many results:</strong> Query may overlap with existing prior art - consider making it more specific.
</Typography.Paragraph>
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
<strong>Few/no results:</strong> Potentially novel concept - good candidate for further exploration.
</Typography.Paragraph>
</div>
)}
{activeTab === 'deduplication' && (
<div style={{ padding: 16 }}>
<Typography.Title level={5} style={{ marginBottom: 16 }}>

View File

@@ -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: <CheckCircleOutlined />, color: 'green', text: 'Active' };
case 'PENDING':
return { icon: <ClockCircleOutlined />, color: 'blue', text: 'Pending' };
case 'DISCONTINUED':
case 'EXPIRED':
return { icon: <CloseCircleOutlined />, color: 'red', text: status };
default:
return { icon: <QuestionCircleOutlined />, color: 'default', text: status || 'Unknown' };
}
}
export function PatentSearchPanel({ descriptions, isDark }: PatentSearchPanelProps) {
const [customQuery, setCustomQuery] = useState('');
const [searchItems, setSearchItems] = useState<SearchItem[]>([]);
const [selectedDescriptions, setSelectedDescriptions] = useState<Set<number>>(new Set());
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
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<SearchResultItem> => {
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 (
<div style={containerStyle}>
{/* Info banner */}
<Card size="small" style={cardStyle}>
<Space>
<GlobalOutlined style={{ color: '#1890ff' }} />
<Text>
Generate search links to check for similar patents on Google Patents or Lens.org
</Text>
</Space>
</Card>
{/* Custom search input */}
<Card size="small" title="Add Custom Search" style={cardStyle}>
<TextArea
placeholder="Enter a description to search for similar patents..."
value={customQuery}
onChange={e => setCustomQuery(e.target.value)}
autoSize={{ minRows: 2, maxRows: 4 }}
style={{ marginBottom: 8 }}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleAddCustomQuery}
disabled={!customQuery.trim()}
>
Add to Search List
</Button>
</Card>
{/* Description selection (if available) */}
{descriptions && descriptions.length > 0 && (
<Card
size="small"
title={`Generated Descriptions (${descriptions.length})`}
style={cardStyle}
extra={
<Button
type="primary"
<div style={{
height: 'calc(100vh - 180px)',
width: '100%',
padding: 16,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'row',
gap: 16,
}}>
{/* Left Column - Search Input & Generated Descriptions */}
<div style={{
flex: '0 0 40%',
minWidth: 0,
display: 'flex',
flexDirection: 'column',
gap: 12,
overflow: 'auto',
}}>
{/* Search input */}
<Card
size="small"
icon={<SearchOutlined />}
onClick={handleAddSelected}
disabled={selectedDescriptions.size === 0}
title={
<Space>
<span>Patent Search</span>
{apiStatus === 'checking' && <Tag color="processing">Checking...</Tag>}
{apiStatus === 'connected' && <Tag color="success">Connected</Tag>}
{apiStatus === 'error' && (
<Tooltip title={`Cannot reach ${window.location.hostname}:8001`}>
<Tag color="error">Unreachable</Tag>
</Tooltip>
)}
</Space>
}
style={cardStyle}
>
Add Selected ({selectedDescriptions.size})
</Button>
}
>
<div style={{ maxHeight: 200, overflow: 'auto' }}>
<Space direction="vertical" style={{ width: '100%' }}>
{descriptions.slice(0, 20).map((desc, idx) => (
<div
key={idx}
onClick={() => toggleDescription(idx)}
style={{
padding: 8,
borderRadius: 4,
cursor: 'pointer',
background: selectedDescriptions.has(idx)
? (isDark ? '#177ddc22' : '#1890ff11')
: (isDark ? '#141414' : '#fafafa'),
border: selectedDescriptions.has(idx)
? `1px solid ${isDark ? '#177ddc' : '#1890ff'}`
: `1px solid ${isDark ? '#303030' : '#f0f0f0'}`,
}}
>
<Space size={4}>
<Tag color="blue" style={{ fontSize: 10 }}>{desc.expert_name}</Tag>
<Tag style={{ fontSize: 10 }}>{desc.keyword}</Tag>
</Space>
<Paragraph
ellipsis={{ rows: 2 }}
style={{ marginBottom: 0, marginTop: 4, fontSize: 12 }}
>
{desc.description}
</Paragraph>
</div>
))}
{descriptions.length > 20 && (
<Text type="secondary">
And {descriptions.length - 20} more descriptions...
</Text>
)}
</Space>
</div>
</Card>
)}
{/* Search list */}
{searchItems.length > 0 && (
<Card
size="small"
title={`Search List (${searchItems.length})`}
style={{ ...cardStyle, flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
extra={
<Button size="small" danger onClick={handleClearAll}>
Clear All
</Button>
}
bodyStyle={{ flex: 1, overflow: 'auto', padding: 0 }}
>
<List
dataSource={searchItems}
renderItem={item => (
<List.Item
style={{
padding: '12px 16px',
borderBottom: `1px solid ${isDark ? '#303030' : '#f0f0f0'}`,
<TextArea
placeholder="Enter a description to search for similar patents... Click a generated description below to load it here for editing."
value={customQuery}
onChange={e => setCustomQuery(e.target.value)}
onPressEnter={e => {
if (!e.shiftKey) {
e.preventDefault();
handleSearchCustom();
}
}}
actions={[
<Tooltip title="Open in Google Patents" key="google">
<Button
type="link"
icon={<LinkOutlined />}
href={item.searchUrl}
target="_blank"
>
Google
</Button>
</Tooltip>,
<Tooltip title="Open in Lens.org" key="lens">
<Button
type="link"
icon={<GlobalOutlined />}
href={generateLensSearchUrl(item.query)}
target="_blank"
>
Lens
</Button>
</Tooltip>,
<Tooltip title="Copy URL" key="copy">
<Button
type="text"
icon={<CopyOutlined />}
onClick={() => handleCopyUrl(item.searchUrl)}
/>
</Tooltip>,
<Tooltip title="Remove" key="remove">
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveItem(item.id)}
/>
</Tooltip>,
]}
autoSize={{ minRows: 3, maxRows: 6 }}
style={{ marginBottom: 8 }}
disabled={isSearching}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleSearchCustom}
disabled={!customQuery.trim()}
loading={isSearching}
block
>
<List.Item.Meta
title={
<Space size={4}>
{item.expertName && (
<Tag color="blue" style={{ fontSize: 10 }}>{item.expertName}</Tag>
)}
{item.keyword && (
<Tag style={{ fontSize: 10 }}>{item.keyword}</Tag>
)}
Search Patents
</Button>
</Card>
{/* Generated Descriptions */}
{descriptions && descriptions.length > 0 && (
<Card
size="small"
title={`Generated Descriptions (${descriptions.length})`}
style={{ ...cardStyle, flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
bodyStyle={{ flex: 1, overflow: 'auto', padding: 8 }}
>
<Space direction="vertical" style={{ width: '100%' }} size={8}>
{descriptions.map((desc, idx) => (
<div
key={idx}
onClick={() => handleSelectDescription(desc)}
style={{
padding: 8,
borderRadius: 4,
cursor: 'pointer',
background: isDark ? '#141414' : '#fafafa',
border: `1px solid ${isDark ? '#303030' : '#f0f0f0'}`,
transition: 'all 0.2s',
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = isDark ? '#177ddc' : '#1890ff';
e.currentTarget.style.background = isDark ? '#177ddc22' : '#1890ff11';
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = isDark ? '#303030' : '#f0f0f0';
e.currentTarget.style.background = isDark ? '#141414' : '#fafafa';
}}
>
<Space size={4} style={{ marginBottom: 4 }}>
<Tag color="blue" style={{ fontSize: 10 }}>{desc.expert_name}</Tag>
<Tag style={{ fontSize: 10 }}>{desc.keyword}</Tag>
<EditOutlined style={{ fontSize: 10, color: isDark ? '#177ddc' : '#1890ff' }} />
</Space>
<Paragraph
ellipsis={{ rows: 2 }}
style={{ marginBottom: 0, fontSize: 12 }}
>
{desc.description}
</Paragraph>
</div>
))}
</Space>
</Card>
)}
{/* Empty state when no descriptions */}
{(!descriptions || descriptions.length === 0) && (
<Card style={{ ...cardStyle, flex: 1 }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Space direction="vertical">
<Text>No generated descriptions available</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
Run expert transformation first to generate descriptions
</Text>
</Space>
}
/>
</Card>
)}
</div>
{/* Right Column - Search Results */}
<div style={{
flex: 1,
minWidth: 0,
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
}}>
<Card
size="small"
title={`Search Results (${searchResults.length} queries)`}
style={{ ...cardStyle, flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
bodyStyle={{ flex: 1, overflow: 'auto', padding: 8 }}
extra={
searchResults.length > 0 && (
<Button size="small" danger onClick={handleClearAll}>
Clear All
</Button>
)
}
>
{searchResults.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Paragraph
ellipsis={{ rows: 2 }}
style={{ marginBottom: 0, fontSize: 12 }}
>
{item.query}
</Paragraph>
<Space direction="vertical">
<Text>No search results yet</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
Enter a query or click a description to search
</Text>
</Space>
}
/>
</List.Item>
)}
/>
</Card>
)}
{/* Empty state */}
{searchItems.length === 0 && (!descriptions || descriptions.length === 0) && (
<Card style={cardStyle}>
<Empty
description={
<Space direction="vertical">
<Text>Enter a description or run transformations first</Text>
<Text type="secondary">
Search links will open in Google Patents or Lens.org
</Text>
</Space>
}
/>
</Card>
)}
{/* Empty state with descriptions but no search items */}
{searchItems.length === 0 && descriptions && descriptions.length > 0 && (
<Card style={cardStyle}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Space direction="vertical">
<Text>Select descriptions above to add to search list</Text>
<Text type="secondary">
Then click the links to search on Google Patents or Lens.org
</Text>
</Space>
}
/>
</Card>
)}
) : (
<Space direction="vertical" style={{ width: '100%' }} size={8}>
{searchResults.map(result => (
<Card
key={result.id}
size="small"
style={{ background: isDark ? '#141414' : '#fafafa' }}
title={
<Space>
<Text style={{ fontSize: 12, maxWidth: 300 }} ellipsis>
{result.query.substring(0, 60)}{result.query.length > 60 ? '...' : ''}
</Text>
<Badge
count={result.totalResults.toLocaleString()}
style={{ backgroundColor: result.error ? '#ff4d4f' : '#52c41a' }}
overflowCount={999999}
/>
</Space>
}
extra={
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveResult(result.id)}
/>
}
>
{result.error ? (
<Text type="danger">{result.error}</Text>
) : result.patents.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Space direction="vertical" size={4}>
<Text strong>No matching patents found</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
This may indicate a novel concept with no existing prior art.
</Text>
</Space>
}
/>
) : (
<List
size="small"
dataSource={result.patents}
renderItem={(patent) => {
const status = getStatusDisplay(patent.legal_status);
return (
<List.Item
style={{
padding: '8px',
borderBottom: `1px solid ${isDark ? '#303030' : '#f0f0f0'}`,
}}
actions={[
<Button
key="open"
type="link"
size="small"
icon={<LinkOutlined />}
href={patent.url}
target="_blank"
/>,
<Button
key="copy"
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyPatent(patent)}
/>,
]}
>
<List.Item.Meta
title={
<Space direction="vertical" size={2}>
<Text strong style={{ fontSize: 13 }}>{patent.title || 'Untitled'}</Text>
<Space size={4} wrap>
<Tag>{patent.jurisdiction}-{patent.doc_number}</Tag>
<Tag color={status.color}>{status.text}</Tag>
{patent.date_published && (
<Text type="secondary" style={{ fontSize: 11 }}>
{patent.date_published}
</Text>
)}
</Space>
</Space>
}
description={
patent.abstract && (
<Paragraph
ellipsis={{ rows: 2, expandable: true, symbol: 'more' }}
style={{ marginBottom: 0, marginTop: 4, fontSize: 12 }}
>
{patent.abstract}
</Paragraph>
)
}
/>
</List.Item>
);
}}
/>
)}
</Card>
))}
</Space>
)}
</Card>
</div>
</div>
);
}

View File

@@ -402,18 +402,22 @@ export interface CrossoverTransformationResult {
transformedIdeas: ExpertTransformationDescription[];
}
// ===== Patent Search types =====
// ===== Patent Search types (Lens.org API) =====
export interface PatentResult {
publication_number: string;
lens_id: string;
doc_number: string;
jurisdiction: string;
kind: string;
title: string;
snippet: string;
publication_date: string | null;
assignee: string | null;
inventor: string | null;
status: 'ACTIVE' | 'NOT_ACTIVE' | 'UNKNOWN';
pdf_url: string | null;
thumbnail_url: string | null;
abstract: string | null;
date_published: string | null;
applicants: string[];
inventors: string[];
legal_status: string | null;
classifications_cpc: string[];
families_simple: string[];
url: string;
}
export interface PatentSearchRequest {