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:
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user